ALwrity version 0.5.4
This commit is contained in:
@@ -13,6 +13,7 @@ import asyncio
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import time
|
import time
|
||||||
|
import re
|
||||||
|
|
||||||
# Import database
|
# Import database
|
||||||
from services.database import get_db_session
|
from services.database import get_db_session
|
||||||
@@ -339,24 +340,138 @@ async def create_enhanced_strategy(
|
|||||||
try:
|
try:
|
||||||
logger.info("🚀 Creating enhanced content strategy")
|
logger.info("🚀 Creating enhanced content strategy")
|
||||||
|
|
||||||
# Validate required fields
|
# Basic required checks
|
||||||
if not strategy_data.get('user_id'):
|
if not strategy_data.get('user_id'):
|
||||||
raise HTTPException(status_code=400, detail="user_id is required")
|
raise HTTPException(status_code=400, detail="user_id is required")
|
||||||
|
|
||||||
if not strategy_data.get('name'):
|
if not strategy_data.get('name'):
|
||||||
raise HTTPException(status_code=400, detail="strategy name is required")
|
raise HTTPException(status_code=400, detail="strategy name is required")
|
||||||
|
|
||||||
# Create enhanced strategy
|
def parse_float(value: Any) -> Optional[float]:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return float(value)
|
||||||
|
if isinstance(value, str):
|
||||||
|
s = value.strip().lower().replace(",", "")
|
||||||
|
# Handle percentage
|
||||||
|
if s.endswith('%'):
|
||||||
|
try:
|
||||||
|
return float(s[:-1])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Handle k/m suffix
|
||||||
|
mul = 1.0
|
||||||
|
if s.endswith('k'):
|
||||||
|
mul = 1_000.0
|
||||||
|
s = s[:-1]
|
||||||
|
elif s.endswith('m'):
|
||||||
|
mul = 1_000_000.0
|
||||||
|
s = s[:-1]
|
||||||
|
m = re.search(r"[-+]?\d*\.?\d+", s)
|
||||||
|
if m:
|
||||||
|
try:
|
||||||
|
return float(m.group(0)) * mul
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parse_int(value: Any) -> Optional[int]:
|
||||||
|
f = parse_float(value)
|
||||||
|
if f is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(round(f))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parse_json(value: Any) -> Optional[Any]:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, (dict, list)):
|
||||||
|
return value
|
||||||
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
return json.loads(value)
|
||||||
|
except Exception:
|
||||||
|
# Accept plain strings in JSON columns
|
||||||
|
return value
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parse_array(value: Any) -> Optional[list]:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, list):
|
||||||
|
return value
|
||||||
|
if isinstance(value, str):
|
||||||
|
# Try JSON first
|
||||||
|
try:
|
||||||
|
j = json.loads(value)
|
||||||
|
if isinstance(j, list):
|
||||||
|
return j
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
parts = [p.strip() for p in value.split(',') if p.strip()]
|
||||||
|
return parts if parts else None
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Coerce and validate fields
|
||||||
|
warnings: Dict[str, str] = {}
|
||||||
|
cleaned = dict(strategy_data)
|
||||||
|
|
||||||
|
# Numerics
|
||||||
|
content_budget = parse_float(strategy_data.get('content_budget'))
|
||||||
|
if strategy_data.get('content_budget') is not None and content_budget is None:
|
||||||
|
warnings['content_budget'] = 'Could not parse number; saved as null'
|
||||||
|
cleaned['content_budget'] = content_budget
|
||||||
|
|
||||||
|
team_size = parse_int(strategy_data.get('team_size'))
|
||||||
|
if strategy_data.get('team_size') is not None and team_size is None:
|
||||||
|
warnings['team_size'] = 'Could not parse integer; saved as null'
|
||||||
|
cleaned['team_size'] = team_size
|
||||||
|
|
||||||
|
# Arrays
|
||||||
|
preferred_formats = parse_array(strategy_data.get('preferred_formats'))
|
||||||
|
if strategy_data.get('preferred_formats') is not None and preferred_formats is None:
|
||||||
|
warnings['preferred_formats'] = 'Could not parse list; saved as null'
|
||||||
|
cleaned['preferred_formats'] = preferred_formats
|
||||||
|
|
||||||
|
# JSON fields
|
||||||
|
json_fields = [
|
||||||
|
'business_objectives','target_metrics','performance_metrics','content_preferences',
|
||||||
|
'consumption_patterns','audience_pain_points','buying_journey','seasonal_trends',
|
||||||
|
'engagement_metrics','top_competitors','competitor_content_strategies','market_gaps',
|
||||||
|
'industry_trends','emerging_trends','content_mix','optimal_timing','quality_metrics',
|
||||||
|
'editorial_guidelines','brand_voice','traffic_sources','conversion_rates','content_roi_targets',
|
||||||
|
'target_audience','content_pillars','ai_recommendations'
|
||||||
|
]
|
||||||
|
for field in json_fields:
|
||||||
|
raw = strategy_data.get(field)
|
||||||
|
parsed = parse_json(raw)
|
||||||
|
# parsed may be a plain string; accept it
|
||||||
|
cleaned[field] = parsed
|
||||||
|
|
||||||
|
# Booleans
|
||||||
|
if 'ab_testing_capabilities' in strategy_data:
|
||||||
|
cleaned['ab_testing_capabilities'] = bool(strategy_data.get('ab_testing_capabilities'))
|
||||||
|
|
||||||
|
# Early return on validation errors
|
||||||
|
if warnings:
|
||||||
|
logger.warning(f"ℹ️ Strategy create warnings: {warnings}")
|
||||||
|
|
||||||
|
# Proceed with create using cleaned data
|
||||||
db_service = EnhancedStrategyDBService(db)
|
db_service = EnhancedStrategyDBService(db)
|
||||||
enhanced_service = EnhancedStrategyService(db_service)
|
enhanced_service = EnhancedStrategyService(db_service)
|
||||||
created_strategy = await enhanced_service.create_enhanced_strategy(strategy_data, db)
|
created_strategy = await enhanced_service.create_enhanced_strategy(cleaned, db)
|
||||||
|
|
||||||
logger.info(f"✅ Enhanced strategy created successfully: {created_strategy.get('id')}")
|
logger.info(f"✅ Enhanced strategy created successfully: {created_strategy.get('id') if isinstance(created_strategy, dict) else getattr(created_strategy,'id', None)}")
|
||||||
|
|
||||||
return ResponseBuilder.create_success_response(
|
resp = ResponseBuilder.create_success_response(
|
||||||
message="Enhanced content strategy created successfully",
|
message="Enhanced content strategy created successfully",
|
||||||
data=created_strategy
|
data=created_strategy
|
||||||
)
|
)
|
||||||
|
if warnings:
|
||||||
|
resp['warnings'] = warnings
|
||||||
|
return resp
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -54,7 +54,18 @@ class AutoFillRefreshService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("AI-only structured generation failed | user=%s | err=%s", user_id, repr(e))
|
logger.error("AI-only structured generation failed | user=%s | err=%s", user_id, repr(e))
|
||||||
logger.error("Traceback:\n%s", traceback.format_exc())
|
logger.error("Traceback:\n%s", traceback.format_exc())
|
||||||
raise
|
# Return graceful fallback instead of raising
|
||||||
|
return {
|
||||||
|
'fields': {},
|
||||||
|
'sources': {},
|
||||||
|
'meta': {
|
||||||
|
'ai_used': False,
|
||||||
|
'ai_overrides_count': 0,
|
||||||
|
'ai_override_fields': [],
|
||||||
|
'ai_only': True,
|
||||||
|
'error': str(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Fallback to previous behavior (DB + sparse overrides)
|
# Fallback to previous behavior (DB + sparse overrides)
|
||||||
payload = await self.autofill.get_autofill(user_id)
|
payload = await self.autofill.get_autofill(user_id)
|
||||||
|
|||||||
@@ -7,27 +7,53 @@ from services.ai_service_manager import AIServiceManager, AIServiceType
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Complete core fields - all 30+ fields that the frontend expects
|
||||||
CORE_FIELDS = [
|
CORE_FIELDS = [
|
||||||
|
# Business Context (8 fields)
|
||||||
'business_objectives', 'target_metrics', 'content_budget', 'team_size', 'implementation_timeline',
|
'business_objectives', 'target_metrics', 'content_budget', 'team_size', 'implementation_timeline',
|
||||||
'market_share','competitive_position','performance_metrics','content_preferences','consumption_patterns',
|
'market_share', 'competitive_position', 'performance_metrics',
|
||||||
'audience_pain_points','buying_journey','seasonal_trends','engagement_metrics','top_competitors',
|
|
||||||
'competitor_content_strategies','market_gaps','industry_trends','emerging_trends','preferred_formats',
|
# Audience Intelligence (6 fields)
|
||||||
'content_mix','content_frequency','optimal_timing','quality_metrics','editorial_guidelines','brand_voice',
|
'content_preferences', 'consumption_patterns', 'audience_pain_points',
|
||||||
|
'buying_journey', 'seasonal_trends', 'engagement_metrics',
|
||||||
|
|
||||||
|
# Competitive Intelligence (5 fields)
|
||||||
|
'top_competitors', 'competitor_content_strategies', 'market_gaps', 'industry_trends', 'emerging_trends',
|
||||||
|
|
||||||
|
# Content Strategy (7 fields)
|
||||||
|
'preferred_formats', 'content_mix', 'content_frequency', 'optimal_timing',
|
||||||
|
'quality_metrics', 'editorial_guidelines', 'brand_voice',
|
||||||
|
|
||||||
|
# Performance & Analytics (4 fields)
|
||||||
'traffic_sources', 'conversion_rates', 'content_roi_targets', 'ab_testing_capabilities'
|
'traffic_sources', 'conversion_rates', 'content_roi_targets', 'ab_testing_capabilities'
|
||||||
]
|
]
|
||||||
|
|
||||||
JSON_FIELDS = {
|
JSON_FIELDS = {
|
||||||
'business_objectives', 'target_metrics', 'content_preferences'
|
'business_objectives', 'target_metrics', 'content_preferences', 'consumption_patterns',
|
||||||
|
'audience_pain_points', 'buying_journey', 'seasonal_trends', 'engagement_metrics',
|
||||||
|
'competitor_content_strategies', 'market_gaps', 'industry_trends', 'emerging_trends',
|
||||||
|
'content_mix', 'optimal_timing', 'quality_metrics', 'editorial_guidelines',
|
||||||
|
'conversion_rates', 'content_roi_targets', 'performance_metrics'
|
||||||
}
|
}
|
||||||
|
|
||||||
ARRAY_FIELDS = {
|
ARRAY_FIELDS = {
|
||||||
'preferred_formats'
|
'preferred_formats', 'top_competitors', 'market_gaps', 'industry_trends', 'traffic_sources'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Select field options mapping for value normalization
|
||||||
|
SELECT_FIELD_OPTIONS = {
|
||||||
|
'implementation_timeline': ['3 months', '6 months', '1 year', '2 years', 'Ongoing'],
|
||||||
|
'competitive_position': ['Leader', 'Challenger', 'Niche', 'Emerging'],
|
||||||
|
'content_frequency': ['Daily', 'Weekly', 'Bi-weekly', 'Monthly', 'Quarterly'],
|
||||||
|
'brand_voice': ['Professional', 'Casual', 'Friendly', 'Authoritative', 'Innovative']
|
||||||
}
|
}
|
||||||
|
|
||||||
class AIStructuredAutofillService:
|
class AIStructuredAutofillService:
|
||||||
"""Generate the complete 30+ Strategy Builder fields strictly from AI using onboarding context only."""
|
"""Generate the complete Strategy Builder fields strictly from AI using onboarding context only."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.ai = AIServiceManager()
|
self.ai = AIServiceManager()
|
||||||
|
self.max_retries = 2 # Maximum retry attempts for malformed JSON
|
||||||
|
|
||||||
def _build_context_summary(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
def _build_context_summary(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
website = context.get('website_analysis') or {}
|
website = context.get('website_analysis') or {}
|
||||||
@@ -73,89 +99,353 @@ class AIStructuredAutofillService:
|
|||||||
return summary
|
return summary
|
||||||
|
|
||||||
def _build_schema(self) -> Dict[str, Any]:
|
def _build_schema(self) -> Dict[str, Any]:
|
||||||
# Build a Gemini SDK-compatible Schema (dict equivalent), not JSON Schema.
|
# Simplified schema following Gemini best practices
|
||||||
# Avoid unsupported keys like oneOf/additionalProperties.
|
# Reduce complexity by flattening nested structures and simplifying constraints
|
||||||
properties: Dict[str, Any] = {}
|
properties: Dict[str, Any] = {}
|
||||||
typed_overrides: Dict[str, Any] = {
|
|
||||||
# Use STRING for complex JSON-bearing fields to avoid OBJECT property constraints
|
# Simplified field definitions - avoid complex constraints that cause 400 errors
|
||||||
'business_objectives': {"type": "STRING"},
|
field_definitions = {
|
||||||
'target_metrics': {"type": "STRING"},
|
# Core business fields (simplified)
|
||||||
'content_preferences': {"type": "STRING"},
|
'business_objectives': {"type": "STRING", "description": "Business goals and objectives"},
|
||||||
# Known arrays
|
'target_metrics': {"type": "STRING", "description": "KPIs and success metrics"},
|
||||||
'preferred_formats': {"type": "ARRAY", "items": {"type": "STRING"}},
|
'content_budget': {"type": "NUMBER", "description": "Monthly content budget in dollars"},
|
||||||
# Known selects
|
'team_size': {"type": "NUMBER", "description": "Number of people in content team"},
|
||||||
'content_frequency': {"type": "STRING"},
|
'implementation_timeline': {"type": "STRING", "description": "Strategy implementation timeline"},
|
||||||
|
'market_share': {"type": "STRING", "description": "Current market share percentage"},
|
||||||
|
'competitive_position': {"type": "STRING", "description": "Market competitive position"},
|
||||||
|
'performance_metrics': {"type": "STRING", "description": "Current performance data"},
|
||||||
|
|
||||||
|
# Audience fields (simplified)
|
||||||
|
'content_preferences': {"type": "STRING", "description": "Content format and topic preferences"},
|
||||||
|
'consumption_patterns': {"type": "STRING", "description": "When and how audience consumes content"},
|
||||||
|
'audience_pain_points': {"type": "STRING", "description": "Key audience challenges and pain points"},
|
||||||
|
'buying_journey': {"type": "STRING", "description": "Customer journey stages and touchpoints"},
|
||||||
|
'seasonal_trends': {"type": "STRING", "description": "Seasonal content patterns and trends"},
|
||||||
|
'engagement_metrics': {"type": "STRING", "description": "Current engagement data and metrics"},
|
||||||
|
|
||||||
|
# Competitive fields (simplified)
|
||||||
|
'top_competitors': {"type": "STRING", "description": "Main competitors"},
|
||||||
|
'competitor_content_strategies': {"type": "STRING", "description": "Analysis of competitor content approaches"},
|
||||||
|
'market_gaps': {"type": "STRING", "description": "Market opportunities and gaps"},
|
||||||
|
'industry_trends': {"type": "STRING", "description": "Current industry trends"},
|
||||||
|
'emerging_trends': {"type": "STRING", "description": "Upcoming trends and opportunities"},
|
||||||
|
|
||||||
|
# Content strategy fields (simplified)
|
||||||
|
'preferred_formats': {"type": "STRING", "description": "Preferred content formats"},
|
||||||
|
'content_mix': {"type": "STRING", "description": "Content mix distribution"},
|
||||||
|
'content_frequency': {"type": "STRING", "description": "Content publishing frequency"},
|
||||||
|
'optimal_timing': {"type": "STRING", "description": "Best times for publishing content"},
|
||||||
|
'quality_metrics': {"type": "STRING", "description": "Content quality standards and metrics"},
|
||||||
|
'editorial_guidelines': {"type": "STRING", "description": "Style and tone guidelines"},
|
||||||
|
'brand_voice': {"type": "STRING", "description": "Brand voice and tone"},
|
||||||
|
|
||||||
|
# Performance fields (simplified)
|
||||||
|
'traffic_sources': {"type": "STRING", "description": "Primary traffic sources"},
|
||||||
|
'conversion_rates': {"type": "STRING", "description": "Target conversion rates and metrics"},
|
||||||
|
'content_roi_targets': {"type": "STRING", "description": "ROI goals and targets for content"},
|
||||||
|
'ab_testing_capabilities': {"type": "BOOLEAN", "description": "Whether A/B testing capabilities are available"}
|
||||||
}
|
}
|
||||||
for key in CORE_FIELDS:
|
|
||||||
properties[key] = typed_overrides.get(key, {"type": "STRING"})
|
# Build properties from field definitions
|
||||||
|
for field_id in CORE_FIELDS:
|
||||||
|
if field_id in field_definitions:
|
||||||
|
properties[field_id] = field_definitions[field_id]
|
||||||
|
else:
|
||||||
|
# Fallback for any missing fields
|
||||||
|
properties[field_id] = {"type": "STRING", "description": f"Value for {field_id}"}
|
||||||
|
|
||||||
|
# Use propertyOrdering as recommended by Gemini docs for consistent output
|
||||||
schema = {
|
schema = {
|
||||||
"type": "OBJECT",
|
"type": "OBJECT",
|
||||||
"properties": properties,
|
"properties": properties,
|
||||||
# Property ordering can help response consistency per Gemini docs
|
"required": CORE_FIELDS, # Make all fields required
|
||||||
"propertyOrdering": CORE_FIELDS,
|
"propertyOrdering": CORE_FIELDS, # Critical for consistent JSON output
|
||||||
|
"description": "Content strategy fields with simplified constraints"
|
||||||
}
|
}
|
||||||
logger.debug("AI Structured Autofill: schema built (SDK) with %d properties", len(CORE_FIELDS))
|
|
||||||
|
logger.debug("AI Structured Autofill: simplified schema built with %d properties and property ordering", len(CORE_FIELDS))
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
def _build_prompt(self, context_summary: Dict[str, Any]) -> str:
|
def _build_prompt(self, context_summary: Dict[str, Any]) -> str:
|
||||||
|
# Ultra-simplified prompt to avoid JSON parsing issues
|
||||||
prompt = (
|
prompt = (
|
||||||
"You are a senior content strategy system. Using ONLY the provided context (do not copy raw\n"
|
"Generate a JSON object with exactly 30 fields for content strategy. Use this exact format:\n\n"
|
||||||
"values), infer professional, actionable values for ALL of the following 30+ strategy fields.\n"
|
|
||||||
"Output strictly valid JSON matching the given schema. Provide concise, business-ready values.\n"
|
'{\n'
|
||||||
"If you are uncertain, infer the most reasonable assumption for a small business. Do not leave\n"
|
'"business_objectives": "Increase traffic and leads",\n'
|
||||||
"fields empty.\n\n"
|
'"target_metrics": "25% growth, 15% conversion",\n'
|
||||||
f"CONTEXT:\n{json.dumps(context_summary, indent=2)}\n\n"
|
'"content_budget": 3000,\n'
|
||||||
"FIELDS TO PRODUCE (keys only; values inferred):\n"
|
'"team_size": 3,\n'
|
||||||
f"{CORE_FIELDS}\n"
|
'"implementation_timeline": "6 months",\n'
|
||||||
|
'"market_share": "15%",\n'
|
||||||
|
'"competitive_position": "Leader",\n'
|
||||||
|
'"performance_metrics": "Current metrics data",\n'
|
||||||
|
'"content_preferences": "Blog posts, videos",\n'
|
||||||
|
'"consumption_patterns": "Peak hours 9-11 AM",\n'
|
||||||
|
'"audience_pain_points": "Time constraints, complexity",\n'
|
||||||
|
'"buying_journey": "Awareness to Decision",\n'
|
||||||
|
'"seasonal_trends": "Q1 planning, Q2 execution",\n'
|
||||||
|
'"engagement_metrics": "3.5% engagement rate",\n'
|
||||||
|
'"top_competitors": "Competitor A, B, C",\n'
|
||||||
|
'"competitor_content_strategies": "Educational content approach",\n'
|
||||||
|
'"market_gaps": "AI tools, automation guides",\n'
|
||||||
|
'"industry_trends": "AI integration, video content",\n'
|
||||||
|
'"emerging_trends": "Voice search, interactive content",\n'
|
||||||
|
'"preferred_formats": "Blog posts, videos, infographics",\n'
|
||||||
|
'"content_mix": "70% educational, 30% promotional",\n'
|
||||||
|
'"content_frequency": "Weekly",\n'
|
||||||
|
'"optimal_timing": "Tuesday/Thursday 10 AM",\n'
|
||||||
|
'"quality_metrics": "SEO score >90, engagement >3%",\n'
|
||||||
|
'"editorial_guidelines": "Professional tone, actionable insights",\n'
|
||||||
|
'"brand_voice": "Professional",\n'
|
||||||
|
'"traffic_sources": "Organic search, social media",\n'
|
||||||
|
'"conversion_rates": "15% conversion, $200 CPA",\n'
|
||||||
|
'"content_roi_targets": "15% conversion, 3:1 ROI",\n'
|
||||||
|
'"ab_testing_capabilities": true\n'
|
||||||
|
'}\n\n'
|
||||||
|
|
||||||
|
f"Business context: {json.dumps(context_summary, indent=2)}\n\n"
|
||||||
|
"Generate the complete JSON with all 30 fields:"
|
||||||
)
|
)
|
||||||
logger.debug("AI Structured Autofill: prompt preview=%d chars", len(prompt))
|
logger.debug("AI Structured Autofill: ultra-simplified prompt (%d chars)", len(prompt))
|
||||||
return prompt
|
return prompt
|
||||||
|
|
||||||
def _normalize_value(self, key: str, value: Any) -> Any:
|
def _normalize_value(self, key: str, value: Any) -> Any:
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
# Parse JSON-bearing fields if they arrived as JSON strings
|
|
||||||
if key in JSON_FIELDS:
|
# Handle numeric fields that might come as text
|
||||||
if isinstance(value, str):
|
if key in ['content_budget', 'team_size']:
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return value
|
||||||
|
elif isinstance(value, str):
|
||||||
|
# Extract numeric value from text
|
||||||
|
import re
|
||||||
|
# Remove currency symbols, commas, and common words
|
||||||
|
cleaned = re.sub(r'[$,€£¥]', '', value.lower())
|
||||||
|
cleaned = re.sub(r'\b(monthly|yearly|annual|people|person|specialist|creator|writer|editor|team|member)\b', '', cleaned)
|
||||||
|
cleaned = re.sub(r'\s+', ' ', cleaned).strip()
|
||||||
|
|
||||||
|
# Extract first number found
|
||||||
|
numbers = re.findall(r'\d+(?:\.\d+)?', cleaned)
|
||||||
|
if numbers:
|
||||||
try:
|
try:
|
||||||
return json.loads(value)
|
num_value = float(numbers[0])
|
||||||
except Exception:
|
# For team_size, convert to integer
|
||||||
# Keep as string if not valid JSON
|
if key == 'team_size':
|
||||||
return value
|
return int(num_value)
|
||||||
return value
|
return num_value
|
||||||
# Coerce arrays from comma-separated strings where applicable
|
except (ValueError, TypeError):
|
||||||
if key in ARRAY_FIELDS:
|
pass
|
||||||
if isinstance(value, str):
|
|
||||||
split = [s.strip() for s in value.split(',') if s.strip()]
|
logger.warning(f"Could not extract numeric value from '{key}' field: '{value}'")
|
||||||
return split if split else None
|
|
||||||
if isinstance(value, list):
|
|
||||||
return [str(v) for v in value]
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Handle boolean fields
|
||||||
|
if key == 'ab_testing_capabilities':
|
||||||
|
if isinstance(value, bool):
|
||||||
return value
|
return value
|
||||||
|
elif isinstance(value, str):
|
||||||
|
normalized_value = value.lower().strip()
|
||||||
|
if normalized_value in ['true', 'yes', 'available', 'enabled', '1']:
|
||||||
|
return True
|
||||||
|
elif normalized_value in ['false', 'no', 'unavailable', 'disabled', '0']:
|
||||||
|
return False
|
||||||
|
logger.warning(f"Could not parse boolean value for '{key}': '{value}'")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Handle select fields with predefined options
|
||||||
|
if key in SELECT_FIELD_OPTIONS:
|
||||||
|
if isinstance(value, str):
|
||||||
|
# Try exact match first (case-insensitive)
|
||||||
|
normalized_value = value.lower().strip()
|
||||||
|
for option in SELECT_FIELD_OPTIONS[key]:
|
||||||
|
if normalized_value == option.lower():
|
||||||
|
return option
|
||||||
|
|
||||||
|
# Try partial matching for common variations
|
||||||
|
for option in SELECT_FIELD_OPTIONS[key]:
|
||||||
|
option_lower = option.lower()
|
||||||
|
# Handle common variations
|
||||||
|
if (normalized_value.startswith(option_lower) or
|
||||||
|
option_lower in normalized_value or
|
||||||
|
normalized_value.endswith(option_lower)):
|
||||||
|
return option
|
||||||
|
|
||||||
|
# Special handling for content_frequency
|
||||||
|
if key == 'content_frequency':
|
||||||
|
if 'daily' in normalized_value:
|
||||||
|
return 'Daily'
|
||||||
|
elif 'weekly' in normalized_value or 'week' in normalized_value:
|
||||||
|
return 'Weekly'
|
||||||
|
elif 'bi-weekly' in normalized_value or 'biweekly' in normalized_value:
|
||||||
|
return 'Bi-weekly'
|
||||||
|
elif 'monthly' in normalized_value or 'month' in normalized_value:
|
||||||
|
return 'Monthly'
|
||||||
|
elif 'quarterly' in normalized_value or 'quarter' in normalized_value:
|
||||||
|
return 'Quarterly'
|
||||||
|
|
||||||
|
# If no match found, return the first option as fallback
|
||||||
|
logger.warning(f"Could not normalize select field '{key}' value: '{value}' to valid options: {SELECT_FIELD_OPTIONS[key]}")
|
||||||
|
return SELECT_FIELD_OPTIONS[key][0] # Return first option as fallback
|
||||||
|
|
||||||
|
# For all other fields, ensure they're strings and not empty
|
||||||
|
if isinstance(value, str):
|
||||||
|
# Special handling for multiselect fields
|
||||||
|
if key in ['preferred_formats', 'top_competitors', 'market_gaps', 'industry_trends', 'traffic_sources']:
|
||||||
|
# Split by comma and clean up each item
|
||||||
|
items = [item.strip() for item in value.split(',') if item.strip()]
|
||||||
|
if items:
|
||||||
|
return items # Return as array for multiselect fields
|
||||||
|
return None
|
||||||
|
return value.strip() if value.strip() else None
|
||||||
|
elif isinstance(value, (int, float, bool)):
|
||||||
|
return str(value)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
# For multiselect fields, return the list as-is
|
||||||
|
if key in ['preferred_formats', 'top_competitors', 'market_gaps', 'industry_trends', 'traffic_sources']:
|
||||||
|
return [str(item) for item in value if item]
|
||||||
|
# For other fields, convert arrays to comma-separated strings
|
||||||
|
return ', '.join(str(item) for item in value if item)
|
||||||
|
else:
|
||||||
|
return str(value) if value else None
|
||||||
|
|
||||||
|
def _calculate_success_rate(self, result: Dict[str, Any]) -> float:
|
||||||
|
"""Calculate the percentage of successfully filled fields."""
|
||||||
|
if not isinstance(result, dict):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
filled_fields = 0
|
||||||
|
for key in CORE_FIELDS:
|
||||||
|
value = result.get(key)
|
||||||
|
if value is not None and value != "" and value != []:
|
||||||
|
# Additional checks for different data types
|
||||||
|
if isinstance(value, str) and value.strip():
|
||||||
|
filled_fields += 1
|
||||||
|
elif isinstance(value, (int, float)) and value != 0:
|
||||||
|
filled_fields += 1
|
||||||
|
elif isinstance(value, bool):
|
||||||
|
filled_fields += 1
|
||||||
|
elif isinstance(value, list) and len(value) > 0:
|
||||||
|
filled_fields += 1
|
||||||
|
elif value is not None and value != "":
|
||||||
|
filled_fields += 1
|
||||||
|
|
||||||
|
return (filled_fields / len(CORE_FIELDS)) * 100
|
||||||
|
|
||||||
|
def _should_retry(self, result: Dict[str, Any], attempt: int) -> bool:
|
||||||
|
"""Determine if we should retry based on success rate and attempt count."""
|
||||||
|
if attempt >= self.max_retries:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if result has error
|
||||||
|
if 'error' in result:
|
||||||
|
logger.info(f"Retry attempt {attempt + 1} due to error: {result.get('error')}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check success rate - stop immediately if we have 100% success
|
||||||
|
success_rate = self._calculate_success_rate(result)
|
||||||
|
logger.info(f"Success rate: {success_rate:.1f}% (attempt {attempt + 1})")
|
||||||
|
|
||||||
|
# If we have 100% success, don't retry
|
||||||
|
if success_rate >= 100.0:
|
||||||
|
logger.info(f"Perfect success rate achieved: {success_rate:.1f}% - no retry needed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Retry if success rate is below 80% (more aggressive than 50%)
|
||||||
|
if success_rate < 80.0:
|
||||||
|
logger.info(f"Retry attempt {attempt + 1} due to low success rate: {success_rate:.1f}% (need 80%+)")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Also retry if we're missing more than 6 fields (20% of 30 fields)
|
||||||
|
missing_count = len([k for k in CORE_FIELDS if not result.get(k) or result.get(k) == "" or result.get(k) == []])
|
||||||
|
if missing_count > 6:
|
||||||
|
logger.info(f"Retry attempt {attempt + 1} due to too many missing fields: {missing_count} missing (max 6)")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
async def generate_autofill_fields(self, user_id: int, context: Dict[str, Any]) -> Dict[str, Any]:
|
async def generate_autofill_fields(self, user_id: int, context: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
context_summary = self._build_context_summary(context)
|
context_summary = self._build_context_summary(context)
|
||||||
schema = self._build_schema()
|
schema = self._build_schema()
|
||||||
prompt = self._build_prompt(context_summary)
|
prompt = self._build_prompt(context_summary)
|
||||||
|
|
||||||
logger.info("AIStructuredAutofillService: generating 30+ fields | user=%s", user_id)
|
logger.info("AIStructuredAutofillService: generating %d fields | user=%s", len(CORE_FIELDS), user_id)
|
||||||
logger.debug("AIStructuredAutofillService: properties=%d", len(schema.get('properties', {})))
|
logger.debug("AIStructuredAutofillService: properties=%d", len(schema.get('properties', {})))
|
||||||
|
|
||||||
|
last_result = None
|
||||||
|
for attempt in range(self.max_retries + 1):
|
||||||
try:
|
try:
|
||||||
|
logger.info(f"AI structured call attempt {attempt + 1}/{self.max_retries + 1}")
|
||||||
result = await self.ai.execute_structured_json_call(
|
result = await self.ai.execute_structured_json_call(
|
||||||
service_type=AIServiceType.STRATEGIC_INTELLIGENCE,
|
service_type=AIServiceType.STRATEGIC_INTELLIGENCE,
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
schema=schema
|
schema=schema
|
||||||
)
|
)
|
||||||
except Exception as e:
|
last_result = result
|
||||||
logger.error("AI structured call failed | user=%s | err=%s", user_id, repr(e))
|
|
||||||
logger.error("Traceback:\n%s", traceback.format_exc())
|
|
||||||
raise
|
|
||||||
|
|
||||||
if not isinstance(result, dict):
|
# Check if we should retry
|
||||||
raise ValueError("AI did not return a structured JSON object")
|
if not self._should_retry(result, attempt):
|
||||||
|
break
|
||||||
|
|
||||||
|
# Add a small delay before retry
|
||||||
|
if attempt < self.max_retries:
|
||||||
|
import asyncio
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"AI structured call failed (attempt {attempt + 1}) | user=%s | err=%s", user_id, repr(e))
|
||||||
|
logger.error("Traceback:\n%s", traceback.format_exc())
|
||||||
|
last_result = {
|
||||||
|
'error': str(e)
|
||||||
|
}
|
||||||
|
if attempt < self.max_retries:
|
||||||
|
import asyncio
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
|
||||||
|
# Process the final result
|
||||||
|
if not isinstance(last_result, dict):
|
||||||
|
logger.warning("AI did not return a structured JSON object, got: %s", type(last_result))
|
||||||
|
return {
|
||||||
|
'fields': {},
|
||||||
|
'sources': {},
|
||||||
|
'meta': {
|
||||||
|
'ai_used': False,
|
||||||
|
'ai_overrides_count': 0,
|
||||||
|
'missing_fields': CORE_FIELDS,
|
||||||
|
'error': f"AI returned {type(last_result)} instead of dict",
|
||||||
|
'attempts': self.max_retries + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if AI returned an error
|
||||||
|
if 'error' in last_result:
|
||||||
|
logger.warning("AI returned error after all attempts: %s", last_result.get('error'))
|
||||||
|
return {
|
||||||
|
'fields': {},
|
||||||
|
'sources': {},
|
||||||
|
'meta': {
|
||||||
|
'ai_used': False,
|
||||||
|
'ai_overrides_count': 0,
|
||||||
|
'missing_fields': CORE_FIELDS,
|
||||||
|
'error': last_result.get('error', 'Unknown AI error'),
|
||||||
|
'attempts': self.max_retries + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try to extract fields from malformed JSON if needed
|
||||||
|
if len(last_result) < len(CORE_FIELDS) * 0.5: # If we got less than 50% of fields
|
||||||
|
logger.warning("AI returned incomplete result, attempting to extract from raw response")
|
||||||
|
# Try to extract key-value pairs from the raw response
|
||||||
|
extracted_result = self._extract_fields_from_raw_response(last_result)
|
||||||
|
if extracted_result and len(extracted_result) > len(last_result):
|
||||||
|
logger.info("Successfully extracted additional fields from raw response")
|
||||||
|
last_result = extracted_result
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.debug("AI structured result keys=%d | sample keys=%s", len(list(result.keys())), list(result.keys())[:8])
|
logger.debug("AI structured result keys=%d | sample keys=%s", len(list(last_result.keys())), list(last_result.keys())[:8])
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -163,14 +453,40 @@ class AIStructuredAutofillService:
|
|||||||
fields: Dict[str, Any] = {}
|
fields: Dict[str, Any] = {}
|
||||||
sources: Dict[str, str] = {}
|
sources: Dict[str, str] = {}
|
||||||
non_null_keys = []
|
non_null_keys = []
|
||||||
|
missing_fields = []
|
||||||
|
|
||||||
for key in CORE_FIELDS:
|
for key in CORE_FIELDS:
|
||||||
raw_value = result.get(key)
|
raw_value = last_result.get(key)
|
||||||
norm_value = self._normalize_value(key, raw_value)
|
norm_value = self._normalize_value(key, raw_value)
|
||||||
if norm_value is not None and norm_value != "" and norm_value != []:
|
if norm_value is not None and norm_value != "" and norm_value != []:
|
||||||
fields[key] = { 'value': norm_value, 'source': 'ai_refresh', 'confidence': 0.8 }
|
fields[key] = { 'value': norm_value, 'source': 'ai_refresh', 'confidence': 0.8 }
|
||||||
sources[key] = 'ai_refresh'
|
sources[key] = 'ai_refresh'
|
||||||
non_null_keys.append(key)
|
non_null_keys.append(key)
|
||||||
missing_fields = [k for k in CORE_FIELDS if k not in non_null_keys]
|
else:
|
||||||
|
missing_fields.append(key)
|
||||||
|
|
||||||
|
# Log detailed field analysis
|
||||||
|
logger.info("AI structured autofill field analysis:")
|
||||||
|
logger.info("✅ Generated fields (%d): %s", len(non_null_keys), non_null_keys)
|
||||||
|
logger.info("❌ Missing fields (%d): %s", len(missing_fields), missing_fields)
|
||||||
|
|
||||||
|
# Categorize missing fields
|
||||||
|
field_categories = {
|
||||||
|
'business_context': ['business_objectives', 'target_metrics', 'content_budget', 'team_size', 'implementation_timeline', 'market_share', 'competitive_position', 'performance_metrics'],
|
||||||
|
'audience_intelligence': ['content_preferences', 'consumption_patterns', 'audience_pain_points', 'buying_journey', 'seasonal_trends', 'engagement_metrics'],
|
||||||
|
'competitive_intelligence': ['top_competitors', 'competitor_content_strategies', 'market_gaps', 'industry_trends', 'emerging_trends'],
|
||||||
|
'content_strategy': ['preferred_formats', 'content_mix', 'content_frequency', 'optimal_timing', 'quality_metrics', 'editorial_guidelines', 'brand_voice'],
|
||||||
|
'performance_analytics': ['traffic_sources', 'conversion_rates', 'content_roi_targets', 'ab_testing_capabilities']
|
||||||
|
}
|
||||||
|
|
||||||
|
for category, category_fields in field_categories.items():
|
||||||
|
generated_in_category = [f for f in category_fields if f in non_null_keys]
|
||||||
|
missing_in_category = [f for f in category_fields if f in missing_fields]
|
||||||
|
logger.info("📊 %s: %d/%d fields generated (%s missing: %s)",
|
||||||
|
category.upper(), len(generated_in_category), len(category_fields),
|
||||||
|
len(missing_in_category), missing_in_category)
|
||||||
|
|
||||||
|
success_rate = self._calculate_success_rate(last_result)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
'fields': fields,
|
'fields': fields,
|
||||||
@@ -180,8 +496,43 @@ class AIStructuredAutofillService:
|
|||||||
'ai_overrides_count': len(non_null_keys),
|
'ai_overrides_count': len(non_null_keys),
|
||||||
'ai_override_fields': non_null_keys,
|
'ai_override_fields': non_null_keys,
|
||||||
'ai_only': True,
|
'ai_only': True,
|
||||||
'missing_fields': missing_fields
|
'missing_fields': missing_fields,
|
||||||
|
'success_rate': success_rate,
|
||||||
|
'attempts': self.max_retries + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.info("AI structured autofill completed | non_null_fields=%d missing=%d", len(non_null_keys), len(missing_fields))
|
logger.info("AI structured autofill completed | non_null_fields=%d missing=%d success_rate=%.1f%% attempts=%d",
|
||||||
|
len(non_null_keys), len(missing_fields), success_rate, self.max_retries + 1)
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
def _extract_fields_from_raw_response(self, result: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Extract fields from malformed JSON response using regex patterns."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Convert result to string for pattern matching
|
||||||
|
result_str = str(result)
|
||||||
|
|
||||||
|
extracted = {}
|
||||||
|
|
||||||
|
# Pattern to match key-value pairs in JSON-like format
|
||||||
|
patterns = [
|
||||||
|
r'"([^"]+)":\s*"([^"]*)"', # String values
|
||||||
|
r'"([^"]+)":\s*(\d+(?:\.\d+)?)', # Numeric values
|
||||||
|
r'"([^"]+)":\s*(true|false)', # Boolean values
|
||||||
|
r'"([^"]+)":\s*\[([^\]]*)\]', # Array values
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
matches = re.findall(pattern, result_str)
|
||||||
|
for key, value in matches:
|
||||||
|
if key in CORE_FIELDS:
|
||||||
|
# Clean up the value
|
||||||
|
if value.lower() in ['true', 'false']:
|
||||||
|
extracted[key] = value.lower() == 'true'
|
||||||
|
elif value.replace('.', '').isdigit():
|
||||||
|
extracted[key] = float(value) if '.' in value else int(value)
|
||||||
|
else:
|
||||||
|
extracted[key] = value.strip('"')
|
||||||
|
|
||||||
|
logger.info("Extracted %d fields from raw response: %s", len(extracted), list(extracted.keys()))
|
||||||
|
return extracted
|
||||||
@@ -802,6 +802,9 @@ class EnhancedStrategyService:
|
|||||||
payload = await service.get_autofill(user_id)
|
payload = await service.get_autofill(user_id)
|
||||||
logger.info(f"Retrieved comprehensive onboarding data for user {user_id}")
|
logger.info(f"Retrieved comprehensive onboarding data for user {user_id}")
|
||||||
return payload
|
return payload
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting onboarding data: {str(e)}")
|
||||||
|
raise
|
||||||
finally:
|
finally:
|
||||||
temp_db.close()
|
temp_db.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1180,4 +1183,3 @@ class EnhancedStrategyService:
|
|||||||
'throughput_metrics': {}
|
'throughput_metrics': {}
|
||||||
}
|
}
|
||||||
# No further action required
|
# No further action required
|
||||||
return
|
|
||||||
@@ -522,7 +522,10 @@ Format as structured JSON with detailed assessment and optimization guidance.
|
|||||||
error_message=error_message
|
error_message=error_message
|
||||||
)
|
)
|
||||||
self.metrics.append(metrics)
|
self.metrics.append(metrics)
|
||||||
raise Exception(error_message)
|
# Don't raise JSON decode errors as fatal - let the calling code handle them
|
||||||
|
# The Gemini provider should have already attempted to repair malformed JSON
|
||||||
|
result = {"error": error_message, "raw_response": str(e)}
|
||||||
|
success = False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_message = f"AI call error for {service_type.value}: {str(e)}"
|
error_message = f"AI call error for {service_type.value}: {str(e)}"
|
||||||
logger.error(error_message)
|
logger.error(error_message)
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ def gemini_structured_json_response(prompt, schema, temperature=0.7, top_p=0.9,
|
|||||||
if parsed:
|
if parsed:
|
||||||
return parsed if isinstance(parsed, dict) else json.loads(json.dumps(parsed))
|
return parsed if isinstance(parsed, dict) else json.loads(json.dumps(parsed))
|
||||||
text = (response.text or '').strip()
|
text = (response.text or '').strip()
|
||||||
|
|
||||||
# Strip markdown code fences if present
|
# Strip markdown code fences if present
|
||||||
if text.startswith('```'):
|
if text.startswith('```'):
|
||||||
# remove leading ```json or ``` and trailing ```
|
# remove leading ```json or ``` and trailing ```
|
||||||
@@ -251,10 +252,14 @@ def gemini_structured_json_response(prompt, schema, temperature=0.7, top_p=0.9,
|
|||||||
if text.endswith('```'):
|
if text.endswith('```'):
|
||||||
text = text[:-3]
|
text = text[:-3]
|
||||||
text = text.strip()
|
text = text.strip()
|
||||||
|
|
||||||
|
# Try direct JSON parsing first
|
||||||
try:
|
try:
|
||||||
return json.loads(text)
|
return json.loads(text)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError as e:
|
||||||
# Fallback: extract likely JSON object substring
|
logger.warning(f"Direct JSON parsing failed: {e}")
|
||||||
|
|
||||||
|
# Fallback 1: Extract likely JSON object substring
|
||||||
first = text.find('{')
|
first = text.find('{')
|
||||||
last = text.rfind('}')
|
last = text.rfind('}')
|
||||||
if first != -1 and last != -1 and last > first:
|
if first != -1 and last != -1 and last > first:
|
||||||
@@ -262,13 +267,34 @@ def gemini_structured_json_response(prompt, schema, temperature=0.7, top_p=0.9,
|
|||||||
try:
|
try:
|
||||||
return json.loads(candidate)
|
return json.loads(candidate)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
logger.warning("JSON object extraction failed, trying regex")
|
||||||
# Final fallback: regex any object
|
|
||||||
|
# Fallback 2: Regex any object
|
||||||
import re
|
import re
|
||||||
match = re.search(r'\{[\s\S]*\}', text)
|
match = re.search(r'\{[\s\S]*\}', text)
|
||||||
if match:
|
if match:
|
||||||
|
try:
|
||||||
return json.loads(match.group(0))
|
return json.loads(match.group(0))
|
||||||
raise
|
except json.JSONDecodeError:
|
||||||
|
logger.warning("Regex JSON extraction failed, trying repair")
|
||||||
|
|
||||||
|
# Fallback 3: Attempt to repair common JSON issues
|
||||||
|
repaired = _repair_json_string(text)
|
||||||
|
if repaired:
|
||||||
|
try:
|
||||||
|
return json.loads(repaired)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.warning("JSON repair failed")
|
||||||
|
|
||||||
|
# Fallback 4: Extract and parse individual key-value pairs
|
||||||
|
extracted = _extract_key_value_pairs(text)
|
||||||
|
if extracted:
|
||||||
|
return extracted
|
||||||
|
|
||||||
|
# Final fallback: return error with raw response for debugging
|
||||||
|
logger.error(f"All JSON parsing attempts failed for text: {text[:200]}...")
|
||||||
|
return {"error": f"Failed to parse JSON response: {e}", "raw_response": text[:500]}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error parsing structured response: {e}")
|
logger.error(f"Error parsing structured response: {e}")
|
||||||
return {"error": f"Failed to parse JSON response: {e}", "raw_response": (response.text or '')}
|
return {"error": f"Failed to parse JSON response: {e}", "raw_response": (response.text or '')}
|
||||||
@@ -276,3 +302,141 @@ def gemini_structured_json_response(prompt, schema, temperature=0.7, top_p=0.9,
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in Gemini Pro structured JSON generation: {e}")
|
logger.error(f"Error in Gemini Pro structured JSON generation: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def _repair_json_string(text: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Attempt to repair common JSON issues in AI responses.
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Remove any non-JSON content before first {
|
||||||
|
start = text.find('{')
|
||||||
|
if start == -1:
|
||||||
|
return None
|
||||||
|
text = text[start:]
|
||||||
|
|
||||||
|
# Remove any content after last }
|
||||||
|
end = text.rfind('}')
|
||||||
|
if end == -1:
|
||||||
|
return None
|
||||||
|
text = text[:end+1]
|
||||||
|
|
||||||
|
# Fix common issues
|
||||||
|
repaired = text
|
||||||
|
|
||||||
|
# 1. Fix unterminated arrays (add missing closing brackets)
|
||||||
|
# Count opening and closing brackets
|
||||||
|
open_brackets = repaired.count('[')
|
||||||
|
close_brackets = repaired.count(']')
|
||||||
|
if open_brackets > close_brackets:
|
||||||
|
# Add missing closing brackets
|
||||||
|
missing_brackets = open_brackets - close_brackets
|
||||||
|
repaired = repaired + ']' * missing_brackets
|
||||||
|
|
||||||
|
# 2. Fix unterminated strings in arrays
|
||||||
|
# Look for patterns like ["item1", "item2" and add missing quote and bracket
|
||||||
|
lines = repaired.split('\n')
|
||||||
|
fixed_lines = []
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
stripped = line.strip()
|
||||||
|
# Check if line ends with an unquoted string in an array
|
||||||
|
if stripped.endswith('"') and i < len(lines) - 1:
|
||||||
|
next_line = lines[i + 1].strip()
|
||||||
|
if next_line.startswith(']'):
|
||||||
|
# This is fine
|
||||||
|
pass
|
||||||
|
elif not next_line.startswith('"') and not next_line.startswith(']'):
|
||||||
|
# Add missing quote and comma
|
||||||
|
line = line + '",'
|
||||||
|
fixed_lines.append(line)
|
||||||
|
repaired = '\n'.join(fixed_lines)
|
||||||
|
|
||||||
|
# 3. Fix unescaped quotes in string values
|
||||||
|
# This is complex - we'll use a simple approach
|
||||||
|
try:
|
||||||
|
# Try to balance quotes by adding missing ones
|
||||||
|
lines = repaired.split('\n')
|
||||||
|
fixed_lines = []
|
||||||
|
for line in lines:
|
||||||
|
# Count quotes in the line
|
||||||
|
quote_count = line.count('"')
|
||||||
|
if quote_count % 2 == 1: # Odd number of quotes
|
||||||
|
# Add a quote at the end if it looks like an incomplete string
|
||||||
|
if ':' in line and line.strip().endswith('"'):
|
||||||
|
line = line + '"'
|
||||||
|
elif ':' in line and not line.strip().endswith('"') and not line.strip().endswith(','):
|
||||||
|
line = line + '",'
|
||||||
|
fixed_lines.append(line)
|
||||||
|
repaired = '\n'.join(fixed_lines)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 4. Remove trailing commas before closing braces/brackets
|
||||||
|
repaired = re.sub(r',(\s*[}\]])', r'\1', repaired)
|
||||||
|
|
||||||
|
# 5. Fix missing commas between object properties
|
||||||
|
repaired = re.sub(r'"(\s*)"', r'",\1"', repaired)
|
||||||
|
|
||||||
|
return repaired
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_key_value_pairs(text: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Extract key-value pairs from malformed JSON text as a last resort.
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
# Look for patterns like "key": "value" or "key": value
|
||||||
|
# This regex looks for quoted keys followed by colons and values
|
||||||
|
pattern = r'"([^"]+)"\s*:\s*(?:"([^"]*)"|([^,}\]]+))'
|
||||||
|
matches = re.findall(pattern, text)
|
||||||
|
|
||||||
|
for key, quoted_value, unquoted_value in matches:
|
||||||
|
value = quoted_value if quoted_value else unquoted_value.strip()
|
||||||
|
|
||||||
|
# Clean up the value - remove any trailing content that looks like the next key
|
||||||
|
# This handles cases where the regex captured too much
|
||||||
|
if value and '"' in value:
|
||||||
|
# Split at the first quote that might be the start of the next key
|
||||||
|
parts = value.split('"')
|
||||||
|
if len(parts) > 1:
|
||||||
|
value = parts[0].strip()
|
||||||
|
|
||||||
|
# Try to parse the value appropriately
|
||||||
|
if value.lower() in ['true', 'false']:
|
||||||
|
result[key] = value.lower() == 'true'
|
||||||
|
elif value.lower() == 'null':
|
||||||
|
result[key] = None
|
||||||
|
elif value.isdigit():
|
||||||
|
result[key] = int(value)
|
||||||
|
elif value.replace('.', '').replace('-', '').isdigit():
|
||||||
|
try:
|
||||||
|
result[key] = float(value)
|
||||||
|
except ValueError:
|
||||||
|
result[key] = value
|
||||||
|
else:
|
||||||
|
result[key] = value
|
||||||
|
|
||||||
|
# Also try to extract array values
|
||||||
|
array_pattern = r'"([^"]+)"\s*:\s*\[([^\]]*)\]'
|
||||||
|
array_matches = re.findall(array_pattern, text)
|
||||||
|
|
||||||
|
for key, array_content in array_matches:
|
||||||
|
# Extract individual array items
|
||||||
|
items = []
|
||||||
|
# Look for quoted strings in the array
|
||||||
|
item_pattern = r'"([^"]*)"'
|
||||||
|
item_matches = re.findall(item_pattern, array_content)
|
||||||
|
for item in item_matches:
|
||||||
|
if item.strip():
|
||||||
|
items.append(item.strip())
|
||||||
|
|
||||||
|
if items:
|
||||||
|
result[key] = items
|
||||||
|
|
||||||
|
return result if result else None
|
||||||
@@ -419,31 +419,69 @@ const ContentStrategyBuilder: React.FC = () => {
|
|||||||
const sources = payload.sources || {};
|
const sources = payload.sources || {};
|
||||||
const inputDataPoints = payload.input_data_points || {};
|
const inputDataPoints = payload.input_data_points || {};
|
||||||
const meta = payload.meta || {};
|
const meta = payload.meta || {};
|
||||||
|
|
||||||
|
console.log('🎯 AI Refresh Result - Payload:', payload);
|
||||||
|
console.log('🎯 AI Refresh Result - Fields:', fields);
|
||||||
|
console.log('🎯 AI Refresh Result - Meta:', meta);
|
||||||
|
|
||||||
const fieldValues: Record<string, any> = {};
|
const fieldValues: Record<string, any> = {};
|
||||||
Object.keys(fields).forEach((fieldId) => {
|
Object.keys(fields).forEach((fieldId) => {
|
||||||
const fieldData = fields[fieldId];
|
const fieldData = fields[fieldId];
|
||||||
if (fieldData && typeof fieldData === 'object' && 'value' in fieldData) {
|
if (fieldData && typeof fieldData === 'object' && 'value' in fieldData) {
|
||||||
fieldValues[fieldId] = fieldData.value;
|
fieldValues[fieldId] = fieldData.value;
|
||||||
|
console.log(`✅ Processed field ${fieldId}:`, fieldData.value);
|
||||||
|
} else {
|
||||||
|
console.log(`❌ Skipped field ${fieldId}:`, fieldData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
useEnhancedStrategyStore.setState((state) => ({
|
|
||||||
|
console.log('🎯 Final fieldValues:', fieldValues);
|
||||||
|
|
||||||
|
useEnhancedStrategyStore.setState((state) => {
|
||||||
|
const newState = {
|
||||||
autoPopulatedFields: { ...state.autoPopulatedFields, ...fieldValues },
|
autoPopulatedFields: { ...state.autoPopulatedFields, ...fieldValues },
|
||||||
dataSources: { ...state.dataSources, ...sources },
|
dataSources: { ...state.dataSources, ...sources },
|
||||||
inputDataPoints,
|
inputDataPoints,
|
||||||
formData: { ...state.formData, ...fieldValues }
|
formData: { ...state.formData, ...fieldValues }
|
||||||
}));
|
};
|
||||||
if (!meta.ai_used || meta.ai_overrides_count === 0) {
|
console.log('🎯 Updated store state:', newState);
|
||||||
const msg = 'AI did not produce new values. Please try again or complete onboarding data.';
|
return newState;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enhanced success/error messaging based on retry attempts and success rate
|
||||||
|
const attempts = meta.attempts || 1;
|
||||||
|
const successRate = meta.success_rate || 0;
|
||||||
|
const aiOverridesCount = meta.ai_overrides_count || 0;
|
||||||
|
|
||||||
|
if (!meta.ai_used || aiOverridesCount === 0) {
|
||||||
|
const msg = meta.error || 'AI did not produce new values. Please try again or complete onboarding data.';
|
||||||
setError(msg);
|
setError(msg);
|
||||||
setRefreshError(msg);
|
setRefreshError(msg);
|
||||||
setRefreshMessage('No new AI values available.');
|
setRefreshMessage(`No new AI values available. (${attempts} attempt${attempts > 1 ? 's' : ''})`);
|
||||||
|
} else {
|
||||||
|
// Show success message with retry info if applicable
|
||||||
|
if (attempts > 1) {
|
||||||
|
setRefreshMessage(`AI refresh completed successfully! Generated ${aiOverridesCount} fields in ${attempts} attempts (${successRate.toFixed(1)}% success rate).`);
|
||||||
|
} else {
|
||||||
|
setRefreshMessage(`AI refresh completed! Generated ${aiOverridesCount} fields (${successRate.toFixed(1)}% success rate).`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show warning if success rate is low but we got some data
|
||||||
|
if (successRate < 70 && aiOverridesCount > 0) {
|
||||||
|
setRefreshError(`Warning: Only ${successRate.toFixed(1)}% of fields were filled. Some fields may need manual input.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
es.close();
|
es.close();
|
||||||
setAIGenerating(false);
|
setAIGenerating(false);
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
if (!meta || meta.ai_overrides_count > 0) {
|
|
||||||
|
// Clear success message after a delay
|
||||||
|
if (aiOverridesCount > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
setRefreshMessage(null);
|
setRefreshMessage(null);
|
||||||
setRefreshProgress(0);
|
setRefreshProgress(0);
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.type === 'error') {
|
if (data.type === 'error') {
|
||||||
|
|||||||
@@ -254,9 +254,9 @@ const StrategicInputField: React.FC<StrategicInputFieldProps> = ({
|
|||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
traffic_sources: {
|
traffic_sources: {
|
||||||
type: 'json',
|
type: 'multiselect',
|
||||||
label: 'Traffic Sources',
|
label: 'Traffic Sources',
|
||||||
placeholder: 'Define your traffic sources',
|
options: ['Organic Search', 'Social Media', 'Email Marketing', 'Direct Traffic', 'Referral Traffic', 'Paid Search', 'Display Advertising', 'Content Marketing', 'Influencer Marketing', 'Video Platforms'],
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
conversion_rates: {
|
conversion_rates: {
|
||||||
|
|||||||
@@ -496,9 +496,9 @@ export const STRATEGIC_INPUT_FIELDS: StrategicInputField[] = [
|
|||||||
label: 'Traffic Sources',
|
label: 'Traffic Sources',
|
||||||
description: 'Primary traffic sources',
|
description: 'Primary traffic sources',
|
||||||
tooltip: 'Identify your main traffic sources to understand where your audience comes from and optimize accordingly.',
|
tooltip: 'Identify your main traffic sources to understand where your audience comes from and optimize accordingly.',
|
||||||
type: 'json',
|
type: 'multiselect',
|
||||||
required: false,
|
required: false,
|
||||||
placeholder: 'Define traffic sources'
|
options: ['Organic Search', 'Social Media', 'Email Marketing', 'Direct Traffic', 'Referral Traffic', 'Paid Search', 'Display Advertising', 'Content Marketing', 'Influencer Marketing', 'Video Platforms']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'conversion_rates',
|
id: 'conversion_rates',
|
||||||
|
|||||||
Reference in New Issue
Block a user