ALwrity version 0.5.4
This commit is contained in:
@@ -10,7 +10,7 @@ from sqlalchemy.orm import Session
|
||||
from loguru import logger
|
||||
import json
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from collections import defaultdict
|
||||
import time
|
||||
|
||||
@@ -20,6 +20,7 @@ from services.database import get_db_session
|
||||
# Import services
|
||||
from ..services.enhanced_strategy_service import EnhancedStrategyService
|
||||
from ..services.enhanced_strategy_db_service import EnhancedStrategyDBService
|
||||
from ..services.content_strategy.autofill.ai_refresh import AutoFillRefreshService
|
||||
|
||||
# Import models
|
||||
from models.enhanced_strategy_models import EnhancedContentStrategy
|
||||
@@ -156,25 +157,7 @@ async def stream_strategic_intelligence(
|
||||
yield {"type": "progress", "message": "Analyzing market positioning...", "progress": 40}
|
||||
|
||||
if strategies_data.get("status") == "not_found":
|
||||
# Send fallback data
|
||||
fallback_data = {
|
||||
"market_positioning": {
|
||||
"score": 75,
|
||||
"strengths": ["Strong brand voice", "Consistent content quality"],
|
||||
"weaknesses": ["Limited video content", "Slow content production"]
|
||||
},
|
||||
"competitive_advantages": [
|
||||
{"advantage": "AI-powered content creation", "impact": "High", "implementation": "In Progress"},
|
||||
{"advantage": "Data-driven strategy", "impact": "Medium", "implementation": "Complete"}
|
||||
],
|
||||
"strategic_risks": [
|
||||
{"risk": "Content saturation in market", "probability": "Medium", "impact": "High"},
|
||||
{"risk": "Algorithm changes affecting reach", "probability": "High", "impact": "Medium"}
|
||||
]
|
||||
}
|
||||
# Cache the fallback data
|
||||
set_cached_data(cache_key, fallback_data)
|
||||
yield {"type": "result", "status": "success", "data": fallback_data, "progress": 100}
|
||||
yield {"type": "error", "status": "not_ready", "message": "No strategies found. Complete onboarding and create a strategy before generating intelligence.", "progress": 100}
|
||||
return
|
||||
|
||||
# Extract strategic intelligence from first strategy
|
||||
@@ -274,34 +257,7 @@ async def stream_keyword_research(
|
||||
|
||||
# Handle case where gap_analyses is 0, None, or empty
|
||||
if not gap_analyses or gap_analyses == 0 or len(gap_analyses) == 0:
|
||||
# Send fallback data
|
||||
fallback_data = {
|
||||
"trend_analysis": {
|
||||
"high_volume_keywords": [
|
||||
{"keyword": "AI marketing automation", "volume": "10K-100K", "difficulty": "Medium"},
|
||||
{"keyword": "content strategy 2024", "volume": "1K-10K", "difficulty": "Low"},
|
||||
{"keyword": "digital marketing trends", "volume": "10K-100K", "difficulty": "High"}
|
||||
],
|
||||
"trending_keywords": [
|
||||
{"keyword": "AI content generation", "growth": "+45%", "opportunity": "High"},
|
||||
{"keyword": "voice search optimization", "growth": "+32%", "opportunity": "Medium"},
|
||||
{"keyword": "video marketing strategy", "growth": "+28%", "opportunity": "High"}
|
||||
]
|
||||
},
|
||||
"intent_analysis": {
|
||||
"informational": ["how to", "what is", "guide to"],
|
||||
"navigational": ["company name", "brand name", "website"],
|
||||
"transactional": ["buy", "purchase", "download", "sign up"]
|
||||
},
|
||||
"opportunities": [
|
||||
{"keyword": "AI content tools", "search_volume": "5K-10K", "competition": "Low", "cpc": "$2.50"},
|
||||
{"keyword": "content marketing ROI", "search_volume": "1K-5K", "competition": "Medium", "cpc": "$4.20"},
|
||||
{"keyword": "social media strategy", "search_volume": "10K-50K", "competition": "High", "cpc": "$3.80"}
|
||||
]
|
||||
}
|
||||
# Cache the fallback data
|
||||
set_cached_data(cache_key, fallback_data)
|
||||
yield {"type": "result", "status": "success", "data": fallback_data, "progress": 100}
|
||||
yield {"type": "error", "status": "not_ready", "message": "No keyword research data available. Connect data sources or run analysis first.", "progress": 100}
|
||||
return
|
||||
|
||||
# Extract keyword data from first gap analysis
|
||||
@@ -898,4 +854,157 @@ async def regenerate_enhanced_strategy_ai_analysis(
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error regenerating AI analysis: {str(e)}")
|
||||
raise ContentPlanningErrorHandler.handle_general_error(e, "regenerate_enhanced_strategy_ai_analysis")
|
||||
raise ContentPlanningErrorHandler.handle_general_error(e, "regenerate_enhanced_strategy_ai_analysis")
|
||||
|
||||
@router.post("/{strategy_id}/autofill/accept")
|
||||
async def accept_autofill_inputs(
|
||||
strategy_id: int,
|
||||
payload: Dict[str, Any],
|
||||
db: Session = Depends(get_db)
|
||||
) -> Dict[str, Any]:
|
||||
"""Persist end-user accepted auto-fill inputs and associate with the strategy."""
|
||||
try:
|
||||
logger.info(f"🚀 Accepting autofill inputs for strategy: {strategy_id}")
|
||||
user_id = int(payload.get('user_id') or 1)
|
||||
accepted_fields = payload.get('accepted_fields') or {}
|
||||
# Optional transparency bundles
|
||||
sources = payload.get('sources') or {}
|
||||
input_data_points = payload.get('input_data_points') or {}
|
||||
quality_scores = payload.get('quality_scores') or {}
|
||||
confidence_levels = payload.get('confidence_levels') or {}
|
||||
data_freshness = payload.get('data_freshness') or {}
|
||||
|
||||
if not accepted_fields:
|
||||
raise HTTPException(status_code=400, detail="accepted_fields is required")
|
||||
|
||||
db_service = EnhancedStrategyDBService(db)
|
||||
record = await db_service.save_autofill_insights(
|
||||
strategy_id=strategy_id,
|
||||
user_id=user_id,
|
||||
payload={
|
||||
'accepted_fields': accepted_fields,
|
||||
'sources': sources,
|
||||
'input_data_points': input_data_points,
|
||||
'quality_scores': quality_scores,
|
||||
'confidence_levels': confidence_levels,
|
||||
'data_freshness': data_freshness,
|
||||
}
|
||||
)
|
||||
if not record:
|
||||
raise HTTPException(status_code=500, detail="Failed to persist autofill insights")
|
||||
|
||||
return ResponseBuilder.create_success_response(
|
||||
message="Accepted autofill inputs persisted successfully",
|
||||
data={
|
||||
'id': record.id,
|
||||
'strategy_id': record.strategy_id,
|
||||
'user_id': record.user_id,
|
||||
'created_at': record.created_at.isoformat() if getattr(record, 'created_at', None) else None
|
||||
}
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error accepting autofill inputs: {str(e)}")
|
||||
raise ContentPlanningErrorHandler.handle_general_error(e, "accept_autofill_inputs")
|
||||
|
||||
@router.get("/autofill/refresh/stream")
|
||||
async def stream_autofill_refresh(
|
||||
user_id: Optional[int] = Query(None, description="User ID to build auto-fill for"),
|
||||
use_ai: bool = Query(True, description="Use AI augmentation during refresh"),
|
||||
ai_only: bool = Query(False, description="AI-first refresh: return AI overrides when available"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""SSE endpoint to stream steps while generating a fresh auto-fill payload (no DB writes)."""
|
||||
async def refresh_generator():
|
||||
try:
|
||||
actual_user_id = user_id or 1
|
||||
start_time = datetime.utcnow()
|
||||
logger.info(f"🚀 Starting auto-fill refresh stream for user: {actual_user_id}")
|
||||
yield {"type": "status", "phase": "init", "message": "Starting…", "progress": 5}
|
||||
|
||||
refresh_service = AutoFillRefreshService(db)
|
||||
|
||||
# Phase: Collect onboarding context
|
||||
yield {"type": "progress", "phase": "context", "message": "Collecting context…", "progress": 15}
|
||||
# We deliberately do not emit DB-derived values; context is used inside the service
|
||||
|
||||
# Phase: Build prompt
|
||||
yield {"type": "progress", "phase": "prompt", "message": "Preparing prompt…", "progress": 30}
|
||||
|
||||
# Phase: AI call - run in background and heartbeat until completion
|
||||
yield {"type": "progress", "phase": "ai", "message": "Calling AI…", "progress": 45}
|
||||
|
||||
import asyncio
|
||||
ai_task = asyncio.create_task(
|
||||
refresh_service.build_fresh_payload(actual_user_id, use_ai=use_ai, ai_only=ai_only)
|
||||
)
|
||||
|
||||
# Heartbeat loop while AI is running
|
||||
heartbeat_progress = 50
|
||||
while not ai_task.done():
|
||||
elapsed = (datetime.utcnow() - start_time).total_seconds()
|
||||
heartbeat_progress = min(heartbeat_progress + 3, 85)
|
||||
yield {"type": "progress", "phase": "ai_running", "message": f"AI running… {int(elapsed)}s", "progress": heartbeat_progress}
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Retrieve result or error
|
||||
final_payload = await ai_task
|
||||
|
||||
# Phase: Validate & map
|
||||
yield {"type": "progress", "phase": "validate", "message": "Validating…", "progress": 92}
|
||||
|
||||
# Phase: Transparency
|
||||
yield {"type": "progress", "phase": "finalize", "message": "Finalizing…", "progress": 96}
|
||||
|
||||
total_ms = int((datetime.utcnow() - start_time).total_seconds() * 1000)
|
||||
meta = final_payload.get('meta') or {}
|
||||
meta.update({
|
||||
'sse_total_ms': total_ms,
|
||||
'sse_started_at': start_time.isoformat()
|
||||
})
|
||||
final_payload['meta'] = meta
|
||||
|
||||
yield {"type": "result", "status": "success", "data": final_payload, "progress": 100}
|
||||
logger.info(f"✅ Auto-fill refresh stream completed for user: {actual_user_id} in {total_ms} ms")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error in auto-fill refresh stream: {str(e)}")
|
||||
yield {"type": "error", "message": str(e), "timestamp": datetime.utcnow().isoformat()}
|
||||
|
||||
return StreamingResponse(
|
||||
stream_data(refresh_generator()),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Credentials": "true"
|
||||
}
|
||||
)
|
||||
|
||||
@router.post("/autofill/refresh")
|
||||
async def refresh_autofill(
|
||||
user_id: Optional[int] = Query(None, description="User ID to build auto-fill for"),
|
||||
use_ai: bool = Query(True, description="Use AI augmentation during refresh"),
|
||||
ai_only: bool = Query(False, description="AI-first refresh: return AI overrides when available"),
|
||||
db: Session = Depends(get_db)
|
||||
) -> Dict[str, Any]:
|
||||
"""Non-stream endpoint to return a fresh auto-fill payload (no DB writes)."""
|
||||
try:
|
||||
actual_user_id = user_id or 1
|
||||
started = datetime.utcnow()
|
||||
refresh_service = AutoFillRefreshService(db)
|
||||
payload = await refresh_service.build_fresh_payload(actual_user_id, use_ai=use_ai, ai_only=ai_only)
|
||||
total_ms = int((datetime.utcnow() - started).total_seconds() * 1000)
|
||||
meta = payload.get('meta') or {}
|
||||
meta.update({'http_total_ms': total_ms, 'http_started_at': started.isoformat()})
|
||||
payload['meta'] = meta
|
||||
return ResponseBuilder.create_success_response(
|
||||
message="Fresh auto-fill payload generated successfully",
|
||||
data=payload
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error generating fresh auto-fill payload: {str(e)}")
|
||||
raise ContentPlanningErrorHandler.handle_general_error(e, "refresh_autofill")
|
||||
@@ -1,10 +1,18 @@
|
||||
"""
|
||||
AI Analysis Module
|
||||
AI recommendation generation and analysis services.
|
||||
AI recommendation generation and analysis.
|
||||
"""
|
||||
|
||||
from .ai_recommendations import AIRecommendationsService
|
||||
from .prompt_engineering import PromptEngineeringService
|
||||
from .quality_validation import QualityValidationService
|
||||
from .prompt_engineering import PromptEngineeringService
|
||||
from .strategic_intelligence_analyzer import StrategicIntelligenceAnalyzer
|
||||
from .content_distribution_analyzer import ContentDistributionAnalyzer
|
||||
|
||||
__all__ = ['AIRecommendationsService', 'PromptEngineeringService', 'QualityValidationService']
|
||||
__all__ = [
|
||||
'AIRecommendationsService',
|
||||
'QualityValidationService',
|
||||
'PromptEngineeringService',
|
||||
'StrategicIntelligenceAnalyzer',
|
||||
'ContentDistributionAnalyzer'
|
||||
]
|
||||
@@ -14,6 +14,7 @@ from models.enhanced_strategy_models import EnhancedContentStrategy, EnhancedAIA
|
||||
# Import modular components
|
||||
from .prompt_engineering import PromptEngineeringService
|
||||
from .quality_validation import QualityValidationService
|
||||
from .strategic_intelligence_analyzer import StrategicIntelligenceAnalyzer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,6 +24,7 @@ class AIRecommendationsService:
|
||||
def __init__(self):
|
||||
self.prompt_engineering_service = PromptEngineeringService()
|
||||
self.quality_validation_service = QualityValidationService()
|
||||
self.strategic_intelligence_analyzer = StrategicIntelligenceAnalyzer()
|
||||
|
||||
# Analysis types for comprehensive recommendations
|
||||
self.analysis_types = [
|
||||
@@ -33,62 +35,82 @@ class AIRecommendationsService:
|
||||
'content_calendar_optimization'
|
||||
]
|
||||
|
||||
async def generate_comprehensive_recommendations(self, strategy: EnhancedContentStrategy, db: Session) -> None:
|
||||
"""Generate comprehensive AI recommendations using 5 specialized prompts."""
|
||||
async def _call_ai_service(self, prompt: str, analysis_type: str) -> Dict[str, Any]:
|
||||
"""Call AI service to generate recommendations."""
|
||||
try:
|
||||
logger.info(f"Generating comprehensive AI recommendations for strategy: {strategy.id}")
|
||||
# Import AI service manager
|
||||
from services.ai_service_manager import AIServiceManager
|
||||
|
||||
start_time = datetime.utcnow()
|
||||
# Initialize AI service
|
||||
ai_service = AIServiceManager()
|
||||
|
||||
# Generate recommendations for each analysis type
|
||||
ai_recommendations = {}
|
||||
# Generate AI response based on analysis type
|
||||
if analysis_type == "strategic_intelligence":
|
||||
response = await ai_service.generate_strategic_intelligence({
|
||||
"prompt": prompt,
|
||||
"analysis_type": analysis_type
|
||||
})
|
||||
elif analysis_type == "content_recommendations":
|
||||
response = await ai_service.generate_content_recommendations({
|
||||
"prompt": prompt,
|
||||
"analysis_type": analysis_type
|
||||
})
|
||||
elif analysis_type == "market_analysis":
|
||||
response = await ai_service.generate_market_position_analysis({
|
||||
"prompt": prompt,
|
||||
"analysis_type": analysis_type
|
||||
})
|
||||
else:
|
||||
# Default to strategic intelligence
|
||||
response = await ai_service.generate_strategic_intelligence({
|
||||
"prompt": prompt,
|
||||
"analysis_type": analysis_type
|
||||
})
|
||||
|
||||
for analysis_type in self.analysis_types:
|
||||
try:
|
||||
recommendations = await self._generate_specialized_recommendations(
|
||||
strategy, analysis_type, db
|
||||
)
|
||||
ai_recommendations[analysis_type] = recommendations
|
||||
|
||||
# Store individual analysis result
|
||||
analysis_result = EnhancedAIAnalysisResult(
|
||||
user_id=strategy.user_id,
|
||||
strategy_id=strategy.id,
|
||||
analysis_type=analysis_type,
|
||||
comprehensive_insights=recommendations.get('comprehensive_insights'),
|
||||
audience_intelligence=recommendations.get('audience_intelligence'),
|
||||
competitive_intelligence=recommendations.get('competitive_intelligence'),
|
||||
performance_optimization=recommendations.get('performance_optimization'),
|
||||
content_calendar_optimization=recommendations.get('content_calendar_optimization'),
|
||||
onboarding_data_used=strategy.onboarding_data_used,
|
||||
processing_time=(datetime.utcnow() - start_time).total_seconds(),
|
||||
ai_service_status="operational"
|
||||
)
|
||||
|
||||
db.add(analysis_result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating {analysis_type} recommendations: {str(e)}")
|
||||
# Continue with other analysis types
|
||||
|
||||
db.commit()
|
||||
|
||||
# Update strategy with comprehensive AI analysis
|
||||
strategy.comprehensive_ai_analysis = ai_recommendations
|
||||
strategy.strategic_scores = self.quality_validation_service.calculate_strategic_scores(ai_recommendations)
|
||||
strategy.market_positioning = self.quality_validation_service.extract_market_positioning(ai_recommendations)
|
||||
strategy.competitive_advantages = self.quality_validation_service.extract_competitive_advantages(ai_recommendations)
|
||||
strategy.strategic_risks = self.quality_validation_service.extract_strategic_risks(ai_recommendations)
|
||||
strategy.opportunity_analysis = self.quality_validation_service.extract_opportunity_analysis(ai_recommendations)
|
||||
|
||||
db.commit()
|
||||
|
||||
processing_time = (datetime.utcnow() - start_time).total_seconds()
|
||||
logger.info(f"Comprehensive AI recommendations generated in {processing_time:.2f} seconds")
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating comprehensive AI recommendations: {str(e)}")
|
||||
# Don't raise error, just log it as this is enhancement, not core functionality
|
||||
logger.error(f"Error calling AI service: {str(e)}")
|
||||
raise Exception(f"Failed to generate AI recommendations: {str(e)}")
|
||||
|
||||
def _parse_ai_response(self, ai_response: Dict[str, Any], analysis_type: str) -> Dict[str, Any]:
|
||||
return ai_response # parsing now handled downstream
|
||||
|
||||
def get_output_schema(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"required": ["strategy_brief", "channels", "pillars", "plan_30_60_90", "kpis"],
|
||||
"properties": {
|
||||
"strategy_brief": {"type": "object"},
|
||||
"channels": {"type": "array", "items": {"type": "object"}},
|
||||
"pillars": {"type": "array", "items": {"type": "object"}},
|
||||
"plan_30_60_90": {"type": "object"},
|
||||
"kpis": {"type": "object"},
|
||||
"citations": {"type": "array", "items": {"type": "object"}}
|
||||
}
|
||||
}
|
||||
|
||||
async def generate_comprehensive_ai_recommendations(self, strategy: EnhancedContentStrategy, db: Session) -> None:
|
||||
try:
|
||||
# Build centralized prompts per analysis type
|
||||
prompt = self.prompt_engineering_service.create_specialized_prompt(strategy, "comprehensive_strategy")
|
||||
raw = await self._call_ai_service(prompt, "strategic_intelligence")
|
||||
# Validate against schema
|
||||
schema = self.get_output_schema()
|
||||
self.quality_validation_service.validate_against_schema(raw, schema)
|
||||
# Persist
|
||||
result = EnhancedAIAnalysisResult(
|
||||
strategy_id=strategy.id,
|
||||
analysis_type="comprehensive_strategy",
|
||||
result_json=raw,
|
||||
created_at=datetime.utcnow()
|
||||
)
|
||||
db.add(result)
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Comprehensive recommendation generation failed: {str(e)}")
|
||||
raise
|
||||
|
||||
async def _generate_specialized_recommendations(self, strategy: EnhancedContentStrategy, analysis_type: str, db: Session) -> Dict[str, Any]:
|
||||
"""Generate specialized recommendations using specific AI prompts."""
|
||||
@@ -109,64 +131,8 @@ class AIRecommendationsService:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating {analysis_type} recommendations: {str(e)}")
|
||||
return self._get_fallback_recommendations(analysis_type)
|
||||
|
||||
async def _call_ai_service(self, prompt: str, analysis_type: str) -> Dict[str, Any]:
|
||||
"""Call AI service to generate recommendations."""
|
||||
# Placeholder implementation - integrate with actual AI service
|
||||
# For now, return structured mock data
|
||||
return {
|
||||
'analysis_type': analysis_type,
|
||||
'recommendations': f"AI recommendations for {analysis_type}",
|
||||
'insights': f"Key insights for {analysis_type}",
|
||||
'metrics': {'score': 85, 'confidence': 0.9}
|
||||
}
|
||||
|
||||
def _parse_ai_response(self, ai_response: Dict[str, Any], analysis_type: str) -> Dict[str, Any]:
|
||||
"""Parse and structure AI response."""
|
||||
return {
|
||||
'analysis_type': analysis_type,
|
||||
'recommendations': ai_response.get('recommendations', []),
|
||||
'insights': ai_response.get('insights', []),
|
||||
'metrics': ai_response.get('metrics', {}),
|
||||
'confidence_score': ai_response.get('metrics', {}).get('confidence', 0.8)
|
||||
}
|
||||
|
||||
def _get_fallback_recommendations(self, analysis_type: str) -> Dict[str, Any]:
|
||||
"""Get fallback recommendations when AI service fails."""
|
||||
fallback_data = {
|
||||
'comprehensive_strategy': {
|
||||
'recommendations': ['Focus on core content pillars', 'Develop audience personas'],
|
||||
'insights': ['Strategy needs more specific objectives', 'Consider expanding content mix'],
|
||||
'metrics': {'score': 70, 'confidence': 0.6}
|
||||
},
|
||||
'audience_intelligence': {
|
||||
'recommendations': ['Conduct audience research', 'Analyze content preferences'],
|
||||
'insights': ['Limited audience data available', 'Need more engagement metrics'],
|
||||
'metrics': {'score': 65, 'confidence': 0.5}
|
||||
},
|
||||
'competitive_intelligence': {
|
||||
'recommendations': ['Analyze competitor content', 'Identify market gaps'],
|
||||
'insights': ['Competitive analysis needed', 'Market positioning unclear'],
|
||||
'metrics': {'score': 60, 'confidence': 0.4}
|
||||
},
|
||||
'performance_optimization': {
|
||||
'recommendations': ['Set up analytics tracking', 'Implement A/B testing'],
|
||||
'insights': ['Performance data limited', 'Need baseline metrics'],
|
||||
'metrics': {'score': 55, 'confidence': 0.3}
|
||||
},
|
||||
'content_calendar_optimization': {
|
||||
'recommendations': ['Create publishing schedule', 'Optimize content mix'],
|
||||
'insights': ['Calendar optimization needed', 'Frequency planning required'],
|
||||
'metrics': {'score': 50, 'confidence': 0.2}
|
||||
}
|
||||
}
|
||||
|
||||
return fallback_data.get(analysis_type, {
|
||||
'recommendations': ['General strategy improvement needed'],
|
||||
'insights': ['Limited data available for analysis'],
|
||||
'metrics': {'score': 50, 'confidence': 0.3}
|
||||
})
|
||||
# Raise exception instead of returning fallback data
|
||||
raise Exception(f"Failed to generate {analysis_type} recommendations: {str(e)}")
|
||||
|
||||
async def get_latest_ai_analysis(self, strategy_id: int, db: Session) -> Optional[Dict[str, Any]]:
|
||||
"""Get latest AI analysis for a strategy."""
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
"""
|
||||
Content Distribution Analyzer
|
||||
Handles content distribution strategy analysis and optimization.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ContentDistributionAnalyzer:
|
||||
"""Analyzes and generates content distribution strategies."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def analyze_content_distribution(self, preferred_formats: list, content_frequency: str, industry: str, team_size: int) -> Dict[str, Any]:
|
||||
"""Analyze content distribution strategy for personalized insights."""
|
||||
distribution_channels = []
|
||||
|
||||
# Social media platforms
|
||||
if 'video' in preferred_formats:
|
||||
distribution_channels.extend([
|
||||
{
|
||||
"platform": "TikTok",
|
||||
"priority": "High",
|
||||
"content_type": "Short-form video",
|
||||
"posting_frequency": "Daily",
|
||||
"best_practices": ["Use trending sounds", "Create educational content", "Engage with comments"],
|
||||
"free_tools": ["TikTok Creator Studio", "CapCut"],
|
||||
"expected_reach": "10K-100K views per video"
|
||||
},
|
||||
{
|
||||
"platform": "Instagram Reels",
|
||||
"priority": "High",
|
||||
"content_type": "Short-form video",
|
||||
"posting_frequency": "Daily",
|
||||
"best_practices": ["Use trending hashtags", "Create behind-the-scenes content", "Cross-promote"],
|
||||
"free_tools": ["Instagram Insights", "Canva"],
|
||||
"expected_reach": "5K-50K views per reel"
|
||||
}
|
||||
])
|
||||
|
||||
# Blog and written content
|
||||
if 'blog' in preferred_formats or 'article' in preferred_formats:
|
||||
distribution_channels.append({
|
||||
"platform": "Personal Blog/Website",
|
||||
"priority": "High",
|
||||
"content_type": "Long-form articles",
|
||||
"posting_frequency": "Weekly",
|
||||
"best_practices": ["SEO optimization", "Email list building", "Social sharing"],
|
||||
"free_tools": ["WordPress.com", "Medium", "Substack"],
|
||||
"expected_reach": "1K-10K monthly readers"
|
||||
})
|
||||
|
||||
# Podcast distribution
|
||||
distribution_channels.append({
|
||||
"platform": "Podcast",
|
||||
"priority": "Medium",
|
||||
"content_type": "Audio content",
|
||||
"posting_frequency": "Weekly",
|
||||
"best_practices": ["Consistent publishing", "Guest interviews", "Cross-promotion"],
|
||||
"free_tools": ["Anchor", "Spotify for Podcasters", "Riverside"],
|
||||
"expected_reach": "500-5K monthly listeners"
|
||||
})
|
||||
|
||||
# Email newsletter
|
||||
distribution_channels.append({
|
||||
"platform": "Email Newsletter",
|
||||
"priority": "High",
|
||||
"content_type": "Personal updates and insights",
|
||||
"posting_frequency": "Weekly",
|
||||
"best_practices": ["Personal storytelling", "Exclusive content", "Call-to-action"],
|
||||
"free_tools": ["Mailchimp", "ConvertKit", "Substack"],
|
||||
"expected_reach": "100-1K subscribers"
|
||||
})
|
||||
|
||||
return {
|
||||
"distribution_channels": distribution_channels,
|
||||
"optimal_posting_schedule": self._generate_posting_schedule(content_frequency, team_size),
|
||||
"cross_promotion_strategy": self._generate_cross_promotion_strategy(preferred_formats),
|
||||
"content_repurposing_plan": self._generate_repurposing_plan(preferred_formats),
|
||||
"audience_growth_tactics": [
|
||||
"Collaborate with other creators in your niche",
|
||||
"Participate in industry hashtags and challenges",
|
||||
"Create shareable content that provides value",
|
||||
"Engage with your audience in comments and DMs",
|
||||
"Use trending topics to create relevant content"
|
||||
]
|
||||
}
|
||||
|
||||
def _generate_posting_schedule(self, content_frequency: str, team_size: int) -> Dict[str, Any]:
|
||||
"""Generate optimal posting schedule for personalized insights."""
|
||||
if team_size == 1:
|
||||
return {
|
||||
"monday": "Educational content or industry insights",
|
||||
"tuesday": "Behind-the-scenes or personal story",
|
||||
"wednesday": "Problem-solving content or tips",
|
||||
"thursday": "Community engagement or Q&A",
|
||||
"friday": "Weekend inspiration or fun content",
|
||||
"saturday": "Repurpose best-performing content",
|
||||
"sunday": "Planning and content creation"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"monday": "Weekly theme announcement",
|
||||
"tuesday": "Educational content",
|
||||
"wednesday": "Interactive content",
|
||||
"thursday": "Behind-the-scenes",
|
||||
"friday": "Community highlights",
|
||||
"saturday": "Repurposed content",
|
||||
"sunday": "Planning and creation"
|
||||
}
|
||||
|
||||
def _generate_cross_promotion_strategy(self, preferred_formats: list) -> List[str]:
|
||||
"""Generate cross-promotion strategy for personalized insights."""
|
||||
strategies = []
|
||||
|
||||
if 'video' in preferred_formats:
|
||||
strategies.extend([
|
||||
"Share video snippets on Instagram Stories",
|
||||
"Create YouTube Shorts from longer videos",
|
||||
"Cross-post video content to TikTok and Instagram Reels"
|
||||
])
|
||||
|
||||
if 'blog' in preferred_formats or 'article' in preferred_formats:
|
||||
strategies.extend([
|
||||
"Share blog excerpts on LinkedIn",
|
||||
"Create Twitter threads from blog posts",
|
||||
"Turn blog posts into video content"
|
||||
])
|
||||
|
||||
strategies.extend([
|
||||
"Use consistent hashtags across platforms",
|
||||
"Cross-promote content on different platforms",
|
||||
"Create platform-specific content variations",
|
||||
"Share behind-the-scenes content across all platforms"
|
||||
])
|
||||
|
||||
return strategies
|
||||
|
||||
def _generate_repurposing_plan(self, preferred_formats: list) -> Dict[str, List[str]]:
|
||||
"""Generate content repurposing plan for personalized insights."""
|
||||
repurposing_plan = {}
|
||||
|
||||
if 'video' in preferred_formats:
|
||||
repurposing_plan['video_content'] = [
|
||||
"Extract key quotes for social media posts",
|
||||
"Create blog posts from video transcripts",
|
||||
"Turn video clips into GIFs for social media",
|
||||
"Create podcast episodes from video content",
|
||||
"Extract audio for podcast distribution"
|
||||
]
|
||||
|
||||
if 'blog' in preferred_formats or 'article' in preferred_formats:
|
||||
repurposing_plan['written_content'] = [
|
||||
"Create social media posts from blog highlights",
|
||||
"Turn blog posts into video scripts",
|
||||
"Extract quotes for Twitter threads",
|
||||
"Create infographics from blog data",
|
||||
"Turn blog series into email courses"
|
||||
]
|
||||
|
||||
repurposing_plan['general'] = [
|
||||
"Repurpose top-performing content across platforms",
|
||||
"Create different formats for different audiences",
|
||||
"Update and republish evergreen content",
|
||||
"Combine multiple pieces into comprehensive guides",
|
||||
"Extract tips and insights for social media"
|
||||
]
|
||||
|
||||
return repurposing_plan
|
||||
|
||||
def analyze_performance_optimization(self, target_metrics: Dict, content_preferences: Dict, preferred_formats: list, team_size: int) -> Dict[str, Any]:
|
||||
"""Analyze content performance optimization for personalized insights."""
|
||||
optimization_strategies = []
|
||||
|
||||
# Content quality optimization
|
||||
optimization_strategies.append({
|
||||
"strategy": "Content Quality Optimization",
|
||||
"focus_area": "Engagement and retention",
|
||||
"tactics": [
|
||||
"Create content that solves specific problems",
|
||||
"Use storytelling to make content memorable",
|
||||
"Include clear calls-to-action in every piece",
|
||||
"Optimize content length for each platform",
|
||||
"Use data to identify top-performing content types"
|
||||
],
|
||||
"free_tools": ["Google Analytics", "Platform Insights", "A/B Testing"],
|
||||
"expected_improvement": "50% increase in engagement"
|
||||
})
|
||||
|
||||
# SEO optimization
|
||||
optimization_strategies.append({
|
||||
"strategy": "SEO and Discoverability",
|
||||
"focus_area": "Organic reach and traffic",
|
||||
"tactics": [
|
||||
"Research and target relevant keywords",
|
||||
"Optimize titles and descriptions",
|
||||
"Create evergreen content that ranks",
|
||||
"Build backlinks through guest posting",
|
||||
"Improve page load speed and mobile experience"
|
||||
],
|
||||
"free_tools": ["Google Keyword Planner", "Google Search Console", "Yoast SEO"],
|
||||
"expected_improvement": "100% increase in organic traffic"
|
||||
})
|
||||
|
||||
# Audience engagement optimization
|
||||
optimization_strategies.append({
|
||||
"strategy": "Audience Engagement",
|
||||
"focus_area": "Community building and loyalty",
|
||||
"tactics": [
|
||||
"Respond to every comment within 24 hours",
|
||||
"Create interactive content (polls, questions)",
|
||||
"Host live sessions and Q&As",
|
||||
"Share behind-the-scenes content",
|
||||
"Create exclusive content for engaged followers"
|
||||
],
|
||||
"free_tools": ["Instagram Stories", "Twitter Spaces", "YouTube Live"],
|
||||
"expected_improvement": "75% increase in community engagement"
|
||||
})
|
||||
|
||||
# Content distribution optimization
|
||||
optimization_strategies.append({
|
||||
"strategy": "Distribution Optimization",
|
||||
"focus_area": "Reach and visibility",
|
||||
"tactics": [
|
||||
"Post at optimal times for your audience",
|
||||
"Use platform-specific features (Stories, Reels, etc.)",
|
||||
"Cross-promote content across platforms",
|
||||
"Collaborate with other creators",
|
||||
"Participate in trending conversations"
|
||||
],
|
||||
"free_tools": ["Later", "Buffer", "Hootsuite"],
|
||||
"expected_improvement": "200% increase in reach"
|
||||
})
|
||||
|
||||
return {
|
||||
"optimization_strategies": optimization_strategies,
|
||||
"performance_tracking_metrics": [
|
||||
"Engagement rate (likes, comments, shares)",
|
||||
"Reach and impressions",
|
||||
"Click-through rates",
|
||||
"Time spent on content",
|
||||
"Follower growth rate",
|
||||
"Conversion rates (email signups, sales)"
|
||||
],
|
||||
"free_analytics_tools": [
|
||||
"Google Analytics (website traffic)",
|
||||
"Platform Insights (social media)",
|
||||
"Google Search Console (SEO)",
|
||||
"Email marketing analytics",
|
||||
"YouTube Analytics (video performance)"
|
||||
],
|
||||
"optimization_timeline": {
|
||||
"immediate": "Set up tracking and identify baseline metrics",
|
||||
"week_1": "Implement one optimization strategy",
|
||||
"month_1": "Analyze results and adjust strategy",
|
||||
"month_3": "Scale successful tactics and experiment with new ones"
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,45 @@ class QualityValidationService:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def validate_against_schema(self, data: Dict[str, Any], schema: Dict[str, Any]) -> None:
|
||||
"""Validate data against a minimal JSON-like schema definition.
|
||||
Raises ValueError on failure.
|
||||
Schema format example:
|
||||
{"type": "object", "required": ["strategy_brief", "channels"], "properties": {"strategy_brief": {"type": "object"}, "channels": {"type": "array"}}}
|
||||
"""
|
||||
def _check(node, sch, path="$"):
|
||||
t = sch.get("type")
|
||||
if t == "object":
|
||||
if not isinstance(node, dict):
|
||||
raise ValueError(f"Schema error at {path}: expected object")
|
||||
for req in sch.get("required", []):
|
||||
if req not in node or node[req] in (None, ""):
|
||||
raise ValueError(f"Schema error at {path}.{req}: required field missing")
|
||||
for key, sub in sch.get("properties", {}).items():
|
||||
if key in node:
|
||||
_check(node[key], sub, f"{path}.{key}")
|
||||
elif t == "array":
|
||||
if not isinstance(node, list):
|
||||
raise ValueError(f"Schema error at {path}: expected array")
|
||||
item_s = sch.get("items")
|
||||
if item_s:
|
||||
for i, item in enumerate(node):
|
||||
_check(item, item_s, f"{path}[{i}]")
|
||||
elif t == "string":
|
||||
if not isinstance(node, str) or not node.strip():
|
||||
raise ValueError(f"Schema error at {path}: expected non-empty string")
|
||||
elif t == "number":
|
||||
if not isinstance(node, (int, float)):
|
||||
raise ValueError(f"Schema error at {path}: expected number")
|
||||
elif t == "boolean":
|
||||
if not isinstance(node, bool):
|
||||
raise ValueError(f"Schema error at {path}: expected boolean")
|
||||
elif t == "any":
|
||||
return
|
||||
else:
|
||||
return
|
||||
_check(data, schema)
|
||||
|
||||
def calculate_strategic_scores(self, ai_recommendations: Dict[str, Any]) -> Dict[str, float]:
|
||||
"""Calculate strategic performance scores from AI recommendations."""
|
||||
scores = {
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
"""
|
||||
Strategic Intelligence Analyzer
|
||||
Handles comprehensive strategic intelligence analysis and generation.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class StrategicIntelligenceAnalyzer:
|
||||
"""Analyzes and generates comprehensive strategic intelligence."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def analyze_market_positioning(self, business_objectives: Dict, industry: str, content_preferences: Dict, team_size: int) -> Dict[str, Any]:
|
||||
"""Analyze market positioning for personalized insights."""
|
||||
# Calculate positioning score based on multiple factors
|
||||
score = 75 # Base score
|
||||
|
||||
# Adjust based on business objectives
|
||||
if business_objectives.get('brand_awareness'):
|
||||
score += 10
|
||||
if business_objectives.get('lead_generation'):
|
||||
score += 8
|
||||
if business_objectives.get('thought_leadership'):
|
||||
score += 12
|
||||
|
||||
# Adjust based on team size (solopreneurs get bonus for agility)
|
||||
if team_size <= 3:
|
||||
score += 8 # Solopreneurs are more agile
|
||||
elif team_size <= 10:
|
||||
score += 3
|
||||
|
||||
# Adjust based on content preferences
|
||||
if content_preferences.get('video_content'):
|
||||
score += 8
|
||||
if content_preferences.get('interactive_content'):
|
||||
score += 6
|
||||
|
||||
score = min(100, max(0, score))
|
||||
|
||||
return {
|
||||
"score": score,
|
||||
"strengths": [
|
||||
"Agile content production and quick pivots",
|
||||
"Direct connection with audience",
|
||||
"Authentic personal brand voice",
|
||||
"Cost-effective content creation",
|
||||
"Rapid experimentation capabilities"
|
||||
],
|
||||
"weaknesses": [
|
||||
"Limited content production capacity",
|
||||
"Time constraints for content creation",
|
||||
"Limited access to professional tools",
|
||||
"Need for content automation",
|
||||
"Limited reach without paid promotion"
|
||||
],
|
||||
"opportunities": [
|
||||
"Leverage personal brand authenticity",
|
||||
"Focus on niche content areas",
|
||||
"Build community-driven content",
|
||||
"Utilize free content creation tools",
|
||||
"Partner with other creators"
|
||||
],
|
||||
"threats": [
|
||||
"Content saturation in market",
|
||||
"Algorithm changes affecting reach",
|
||||
"Time constraints limiting output",
|
||||
"Competition from larger brands",
|
||||
"Platform dependency risks"
|
||||
]
|
||||
}
|
||||
|
||||
def identify_competitive_advantages(self, business_objectives: Dict, content_preferences: Dict, preferred_formats: list, team_size: int) -> List[Dict[str, Any]]:
|
||||
"""Identify competitive advantages for personalized insights."""
|
||||
try:
|
||||
advantages = []
|
||||
|
||||
# Analyze business objectives for competitive advantages
|
||||
if business_objectives.get('lead_generation'):
|
||||
advantages.append({
|
||||
"advantage": "Direct lead generation capabilities",
|
||||
"description": "Ability to create content that directly converts visitors to leads",
|
||||
"impact": "High",
|
||||
"implementation": "Focus on lead magnets and conversion-optimized content",
|
||||
"roi_potential": "300% return on investment",
|
||||
"differentiation": "Personal connection vs corporate approach"
|
||||
})
|
||||
|
||||
if business_objectives.get('brand_awareness'):
|
||||
advantages.append({
|
||||
"advantage": "Authentic personal brand voice",
|
||||
"description": "Unique personal perspective that builds trust and connection",
|
||||
"impact": "High",
|
||||
"implementation": "Share personal stories and behind-the-scenes content",
|
||||
"roi_potential": "250% return on investment",
|
||||
"differentiation": "Authenticity vs polished corporate messaging"
|
||||
})
|
||||
|
||||
if business_objectives.get('thought_leadership'):
|
||||
advantages.append({
|
||||
"advantage": "Niche expertise and authority",
|
||||
"description": "Deep knowledge in specific areas that positions you as the go-to expert",
|
||||
"impact": "Very High",
|
||||
"implementation": "Create comprehensive, educational content in your niche",
|
||||
"roi_potential": "400% return on investment",
|
||||
"differentiation": "Specialized expertise vs generalist approach"
|
||||
})
|
||||
|
||||
# Analyze content preferences for advantages
|
||||
if content_preferences.get('video_content'):
|
||||
advantages.append({
|
||||
"advantage": "Video content expertise",
|
||||
"description": "Ability to create engaging video content that drives higher engagement",
|
||||
"impact": "High",
|
||||
"implementation": "Focus on short-form video platforms (TikTok, Instagram Reels)",
|
||||
"roi_potential": "400% return on investment",
|
||||
"differentiation": "Visual storytelling vs text-only content"
|
||||
})
|
||||
|
||||
if content_preferences.get('interactive_content'):
|
||||
advantages.append({
|
||||
"advantage": "Interactive content capabilities",
|
||||
"description": "Ability to create content that engages and involves the audience",
|
||||
"impact": "Medium",
|
||||
"implementation": "Use polls, questions, and interactive elements",
|
||||
"roi_potential": "200% return on investment",
|
||||
"differentiation": "Two-way communication vs one-way broadcasting"
|
||||
})
|
||||
|
||||
# Analyze team size advantages
|
||||
if team_size == 1:
|
||||
advantages.append({
|
||||
"advantage": "Agility and quick pivots",
|
||||
"description": "Ability to respond quickly to trends and opportunities",
|
||||
"impact": "High",
|
||||
"implementation": "Stay current with trends and adapt content quickly",
|
||||
"roi_potential": "150% return on investment",
|
||||
"differentiation": "Speed vs corporate approval processes"
|
||||
})
|
||||
|
||||
# Analyze preferred formats for advantages
|
||||
if 'video' in preferred_formats:
|
||||
advantages.append({
|
||||
"advantage": "Multi-platform video presence",
|
||||
"description": "Ability to create video content for multiple platforms",
|
||||
"impact": "High",
|
||||
"implementation": "Repurpose video content across TikTok, Instagram, YouTube",
|
||||
"roi_potential": "350% return on investment",
|
||||
"differentiation": "Visual engagement vs static content"
|
||||
})
|
||||
|
||||
if 'blog' in preferred_formats or 'article' in preferred_formats:
|
||||
advantages.append({
|
||||
"advantage": "SEO-optimized content creation",
|
||||
"description": "Ability to create content that ranks well in search engines",
|
||||
"impact": "High",
|
||||
"implementation": "Focus on keyword research and SEO best practices",
|
||||
"roi_potential": "300% return on investment",
|
||||
"differentiation": "Organic reach vs paid advertising"
|
||||
})
|
||||
|
||||
# If no specific advantages found, provide general ones
|
||||
if not advantages:
|
||||
advantages = [
|
||||
{
|
||||
"advantage": "Personal connection and authenticity",
|
||||
"description": "Ability to build genuine relationships with your audience",
|
||||
"impact": "High",
|
||||
"implementation": "Share personal stories and be transparent",
|
||||
"roi_potential": "250% return on investment",
|
||||
"differentiation": "Authentic voice vs corporate messaging"
|
||||
},
|
||||
{
|
||||
"advantage": "Niche expertise",
|
||||
"description": "Deep knowledge in your specific area of expertise",
|
||||
"impact": "High",
|
||||
"implementation": "Focus on your unique knowledge and experience",
|
||||
"roi_potential": "300% return on investment",
|
||||
"differentiation": "Specialized knowledge vs generalist approach"
|
||||
}
|
||||
]
|
||||
|
||||
return advantages
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating competitive advantages: {str(e)}")
|
||||
raise Exception(f"Failed to generate competitive advantages: {str(e)}")
|
||||
|
||||
def assess_strategic_risks(self, industry: str, market_gaps: list, team_size: int, content_frequency: str) -> List[Dict[str, Any]]:
|
||||
"""Assess strategic risks for personalized insights."""
|
||||
risks = []
|
||||
|
||||
# Content saturation risk
|
||||
risks.append({
|
||||
"risk": "Content saturation in market",
|
||||
"probability": "Medium",
|
||||
"impact": "High",
|
||||
"mitigation": "Focus on unique personal perspective and niche topics",
|
||||
"monitoring": "Track content performance vs competitors, monitor engagement rates",
|
||||
"timeline": "Ongoing",
|
||||
"resources_needed": "Free competitive analysis tools"
|
||||
})
|
||||
|
||||
# Algorithm changes risk
|
||||
risks.append({
|
||||
"risk": "Algorithm changes affecting reach",
|
||||
"probability": "High",
|
||||
"impact": "Medium",
|
||||
"mitigation": "Diversify content formats and platforms, build owned audience",
|
||||
"monitoring": "Monitor platform algorithm updates, track reach changes",
|
||||
"timeline": "Ongoing",
|
||||
"resources_needed": "Free multi-platform strategy"
|
||||
})
|
||||
|
||||
# Time constraints risk
|
||||
if team_size == 1:
|
||||
risks.append({
|
||||
"risk": "Time constraints limiting content output",
|
||||
"probability": "High",
|
||||
"impact": "High",
|
||||
"mitigation": "Implement content batching, repurposing, and automation",
|
||||
"monitoring": "Track content creation time, monitor output consistency",
|
||||
"timeline": "1-2 months",
|
||||
"resources_needed": "Free content planning tools"
|
||||
})
|
||||
|
||||
# Platform dependency risk
|
||||
risks.append({
|
||||
"risk": "Platform dependency risks",
|
||||
"probability": "Medium",
|
||||
"impact": "Medium",
|
||||
"mitigation": "Build owned audience through email lists and personal websites",
|
||||
"monitoring": "Track platform-specific vs owned audience growth",
|
||||
"timeline": "3-6 months",
|
||||
"resources_needed": "Free email marketing tools"
|
||||
})
|
||||
|
||||
return risks
|
||||
|
||||
def analyze_opportunities(self, business_objectives: Dict, market_gaps: list, preferred_formats: list) -> List[Dict[str, Any]]:
|
||||
"""Analyze opportunities for personalized insights."""
|
||||
opportunities = []
|
||||
|
||||
# Video content opportunity
|
||||
if 'video' not in preferred_formats:
|
||||
opportunities.append({
|
||||
"opportunity": "Video content expansion",
|
||||
"potential_impact": "High",
|
||||
"implementation_ease": "Medium",
|
||||
"timeline": "1-2 months",
|
||||
"resource_requirements": "Free video tools (TikTok, Instagram Reels, YouTube Shorts)",
|
||||
"roi_potential": "400% return on investment",
|
||||
"description": "Video content generates 4x more engagement than text-only content"
|
||||
})
|
||||
|
||||
# Podcast opportunity
|
||||
opportunities.append({
|
||||
"opportunity": "Start a podcast",
|
||||
"potential_impact": "High",
|
||||
"implementation_ease": "Medium",
|
||||
"timeline": "2-3 months",
|
||||
"resource_requirements": "Free podcast hosting platforms",
|
||||
"roi_potential": "500% return on investment",
|
||||
"description": "Podcasts build deep audience relationships and establish thought leadership"
|
||||
})
|
||||
|
||||
# Newsletter opportunity
|
||||
opportunities.append({
|
||||
"opportunity": "Email newsletter",
|
||||
"potential_impact": "High",
|
||||
"implementation_ease": "High",
|
||||
"timeline": "1 month",
|
||||
"resource_requirements": "Free email marketing tools",
|
||||
"roi_potential": "600% return on investment",
|
||||
"description": "Direct email communication builds owned audience and drives conversions"
|
||||
})
|
||||
|
||||
# Market gap opportunities
|
||||
for gap in market_gaps[:3]: # Top 3 gaps
|
||||
opportunities.append({
|
||||
"opportunity": f"Address market gap: {gap}",
|
||||
"potential_impact": "High",
|
||||
"implementation_ease": "Medium",
|
||||
"timeline": "2-4 months",
|
||||
"resource_requirements": "Free content research and creation",
|
||||
"roi_potential": "300% return on investment",
|
||||
"description": f"Filling the {gap} gap positions you as the go-to expert"
|
||||
})
|
||||
|
||||
return opportunities
|
||||
|
||||
def calculate_performance_metrics(self, target_metrics: Dict, team_size: int) -> Dict[str, Any]:
|
||||
"""Calculate performance metrics for personalized insights."""
|
||||
# Base metrics
|
||||
content_quality_score = 8.5
|
||||
engagement_rate = 4.2
|
||||
conversion_rate = 2.8
|
||||
roi_per_content = 320
|
||||
brand_awareness_score = 7.8
|
||||
|
||||
# Adjust based on team size (solopreneurs get bonus for authenticity)
|
||||
if team_size == 1:
|
||||
content_quality_score += 0.5 # Authenticity bonus
|
||||
engagement_rate += 0.3 # Personal connection
|
||||
elif team_size <= 3:
|
||||
content_quality_score += 0.2
|
||||
engagement_rate += 0.1
|
||||
|
||||
return {
|
||||
"content_quality_score": round(content_quality_score, 1),
|
||||
"engagement_rate": round(engagement_rate, 1),
|
||||
"conversion_rate": round(conversion_rate, 1),
|
||||
"roi_per_content": round(roi_per_content, 0),
|
||||
"brand_awareness_score": round(brand_awareness_score, 1),
|
||||
"content_efficiency": round(roi_per_content / 100 * 100, 1), # Normalized for solopreneurs
|
||||
"personal_brand_strength": round(brand_awareness_score * 1.2, 1) # Personal brand metric
|
||||
}
|
||||
|
||||
def generate_solopreneur_recommendations(self, business_objectives: Dict, team_size: int, preferred_formats: list, industry: str) -> List[Dict[str, Any]]:
|
||||
"""Generate personalized recommendations based on user data."""
|
||||
recommendations = []
|
||||
|
||||
# High priority recommendations
|
||||
if 'video' not in preferred_formats:
|
||||
recommendations.append({
|
||||
"priority": "High",
|
||||
"action": "Start creating short-form video content",
|
||||
"impact": "Increase engagement by 400% and reach by 300%",
|
||||
"timeline": "1 month",
|
||||
"resources_needed": "Free - use TikTok, Instagram Reels, YouTube Shorts",
|
||||
"roi_estimate": "400% return on investment",
|
||||
"implementation_steps": [
|
||||
"Download TikTok and Instagram apps",
|
||||
"Study trending content in your niche",
|
||||
"Create 3-5 short videos per week",
|
||||
"Engage with comments and build community"
|
||||
]
|
||||
})
|
||||
|
||||
# Email list building
|
||||
recommendations.append({
|
||||
"priority": "High",
|
||||
"action": "Build an email list",
|
||||
"impact": "Create owned audience, increase conversions by 200%",
|
||||
"timeline": "2 months",
|
||||
"resources_needed": "Free - use Mailchimp or ConvertKit free tier",
|
||||
"roi_estimate": "600% return on investment",
|
||||
"implementation_steps": [
|
||||
"Sign up for free email marketing tool",
|
||||
"Create lead magnet (free guide, checklist)",
|
||||
"Add signup forms to your content",
|
||||
"Send weekly valuable emails"
|
||||
]
|
||||
})
|
||||
|
||||
# Content batching
|
||||
if team_size == 1:
|
||||
recommendations.append({
|
||||
"priority": "High",
|
||||
"action": "Implement content batching",
|
||||
"impact": "Save 10 hours per week, increase output by 300%",
|
||||
"timeline": "2 weeks",
|
||||
"resources_needed": "Free - use Google Calendar and Notion",
|
||||
"roi_estimate": "300% return on investment",
|
||||
"implementation_steps": [
|
||||
"Block 4-hour content creation sessions",
|
||||
"Create content themes for each month",
|
||||
"Batch similar content types together",
|
||||
"Schedule content in advance"
|
||||
]
|
||||
})
|
||||
|
||||
# Medium priority recommendations
|
||||
recommendations.append({
|
||||
"priority": "Medium",
|
||||
"action": "Optimize for search engines",
|
||||
"impact": "Increase organic traffic by 200%",
|
||||
"timeline": "2 months",
|
||||
"resources_needed": "Free - use Google Keyword Planner",
|
||||
"roi_estimate": "200% return on investment",
|
||||
"implementation_steps": [
|
||||
"Research keywords in your niche",
|
||||
"Optimize existing content for target keywords",
|
||||
"Create SEO-optimized content calendar",
|
||||
"Monitor search rankings"
|
||||
]
|
||||
})
|
||||
|
||||
# Community building
|
||||
recommendations.append({
|
||||
"priority": "Medium",
|
||||
"action": "Build community engagement",
|
||||
"impact": "Increase loyalty and word-of-mouth by 150%",
|
||||
"timeline": "3 months",
|
||||
"resources_needed": "Free - use existing social platforms",
|
||||
"roi_estimate": "150% return on investment",
|
||||
"implementation_steps": [
|
||||
"Respond to every comment and message",
|
||||
"Create community challenges or contests",
|
||||
"Host live Q&A sessions",
|
||||
"Collaborate with other creators"
|
||||
]
|
||||
})
|
||||
|
||||
return recommendations
|
||||
@@ -0,0 +1,4 @@
|
||||
# Dedicated auto-fill package for Content Strategy Builder inputs
|
||||
# Exposes AutoFillService for orchestrating onboarding data → normalized → transformed → frontend fields
|
||||
|
||||
from .autofill_service import AutoFillService
|
||||
@@ -0,0 +1,141 @@
|
||||
from typing import Any, Dict, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from .autofill_service import AutoFillService
|
||||
from ...ai_analytics_service import ContentPlanningAIAnalyticsService
|
||||
from .ai_structured_autofill import AIStructuredAutofillService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AutoFillRefreshService:
|
||||
"""Generates a fresh auto-fill payload for the Strategy Builder.
|
||||
This service does NOT persist anything. Intended for refresh flows.
|
||||
"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
self.autofill = AutoFillService(db)
|
||||
self.ai_analytics = ContentPlanningAIAnalyticsService()
|
||||
self.structured_ai = AIStructuredAutofillService()
|
||||
|
||||
async def build_fresh_payload(self, user_id: int, use_ai: bool = True, ai_only: bool = False) -> Dict[str, Any]:
|
||||
"""Build a fresh auto-fill payload.
|
||||
- Reads latest onboarding-integrated data
|
||||
- Optionally augments with AI overrides (hook, not persisted)
|
||||
- Returns payload in the same shape as AutoFillService.get_autofill, plus meta
|
||||
"""
|
||||
# Base context from onboarding analysis (used for AI context only when ai_only)
|
||||
logger.debug("AutoFillRefreshService: processing onboarding context | user=%s", user_id)
|
||||
base_context = await self.autofill.integration.process_onboarding_data(user_id, self.db)
|
||||
logger.debug(
|
||||
"AutoFillRefreshService: context keys=%s | website=%s research=%s api=%s session=%s",
|
||||
list(base_context.keys()) if isinstance(base_context, dict) else 'n/a',
|
||||
bool((base_context or {}).get('website_analysis')),
|
||||
bool((base_context or {}).get('research_preferences')),
|
||||
bool((base_context or {}).get('api_keys_data')),
|
||||
bool((base_context or {}).get('onboarding_session')),
|
||||
)
|
||||
try:
|
||||
w = (base_context or {}).get('website_analysis') or {}
|
||||
r = (base_context or {}).get('research_preferences') or {}
|
||||
logger.debug("AutoFillRefreshService: website keys=%s | research keys=%s", len(list(w.keys())) if hasattr(w,'keys') else 0, len(list(r.keys())) if hasattr(r,'keys') else 0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if ai_only and use_ai:
|
||||
logger.info("AutoFillRefreshService: AI-only refresh enabled; generating full 30+ fields via AI")
|
||||
try:
|
||||
ai_payload = await self.structured_ai.generate_autofill_fields(user_id, base_context)
|
||||
meta = ai_payload.get('meta') or {}
|
||||
logger.info("AI-only payload meta: ai_used=%s overrides=%s", meta.get('ai_used'), meta.get('ai_overrides_count'))
|
||||
return ai_payload
|
||||
except Exception as e:
|
||||
logger.error("AI-only structured generation failed | user=%s | err=%s", user_id, repr(e))
|
||||
logger.error("Traceback:\n%s", traceback.format_exc())
|
||||
raise
|
||||
|
||||
# Fallback to previous behavior (DB + sparse overrides)
|
||||
payload = await self.autofill.get_autofill(user_id)
|
||||
logger.info("AutoFillRefreshService: Base payload fields: %d", len(payload.get('fields', {})))
|
||||
|
||||
ai_overrides: Dict[str, Any] = {}
|
||||
if use_ai:
|
||||
# Hook to integrate AI-generated overrides for certain fields, if available
|
||||
ai_overrides = await self._generate_ai_overrides(user_id, payload)
|
||||
if ai_overrides:
|
||||
logger.debug("AutoFillRefreshService: merging %d AI overrides", len(ai_overrides))
|
||||
# Merge AI overrides into fields while preserving sources/transparency
|
||||
fields = payload.get('fields', {})
|
||||
for key, override_value in ai_overrides.items():
|
||||
if key in fields and isinstance(fields[key], dict):
|
||||
fields[key]['value'] = override_value
|
||||
else:
|
||||
fields[key] = {'value': override_value, 'source': 'ai_refresh', 'confidence': 0.8}
|
||||
payload['fields'] = fields
|
||||
|
||||
# Label sources for overridden fields as coming from AI refresh (non-persistent)
|
||||
sources = payload.get('sources', {})
|
||||
for key in ai_overrides.keys():
|
||||
sources[key] = 'ai_refresh'
|
||||
payload['sources'] = sources
|
||||
|
||||
# If ai_only requested, we still keep onboarding values where AI is silent (fallback), but we track AI usage
|
||||
overridden_keys = list(ai_overrides.keys())
|
||||
payload['meta'] = {
|
||||
'ai_used': len(overridden_keys) > 0,
|
||||
'ai_overrides_count': len(overridden_keys),
|
||||
'ai_override_fields': overridden_keys,
|
||||
'ai_only': ai_only,
|
||||
}
|
||||
|
||||
logger.info("AutoFillRefreshService: Applied AI overrides for %d fields: %s", len(ai_overrides), overridden_keys)
|
||||
return payload
|
||||
|
||||
async def _generate_ai_overrides(self, user_id: int, base_payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Produce AI overrides for selected fields based on current context.
|
||||
Calls AI analytics with force refresh to avoid stale DB values.
|
||||
Logs raw AI response and mapped overrides for transparency.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"AutoFillRefreshService: Invoking AI analytics for user {user_id} with force refresh")
|
||||
ai_resp = await self.ai_analytics.get_ai_analytics(user_id=user_id, strategy_id=None, force_refresh=True) # type: ignore
|
||||
# Log high-level response structure
|
||||
if isinstance(ai_resp, dict):
|
||||
keys = list(ai_resp.keys())
|
||||
logger.info(f"AI analytics response keys: {keys}")
|
||||
# Optionally log truncated insights/recommendations
|
||||
insights = ai_resp.get('insights')
|
||||
recs = ai_resp.get('recommendations')
|
||||
if insights is not None:
|
||||
logger.info(f"AI insights count: {len(insights) if hasattr(insights, '__len__') else 'n/a'}")
|
||||
if recs is not None:
|
||||
logger.info(f"AI recommendations count: {len(recs) if hasattr(recs, '__len__') else 'n/a'}")
|
||||
else:
|
||||
logger.warning("AI analytics response is not a dict; skipping mapping")
|
||||
return {}
|
||||
|
||||
# Minimal, conservative mapping attempt (only if safely found)
|
||||
overrides: Dict[str, Any] = {}
|
||||
# Example: try to map preferred_formats from recommendations if present
|
||||
try:
|
||||
recs = ai_resp.get('recommendations') or {}
|
||||
if isinstance(recs, dict):
|
||||
pf = recs.get('preferred_formats')
|
||||
if pf:
|
||||
overrides['preferred_formats'] = pf
|
||||
# Example: target_metrics from insights/metrics if present
|
||||
insights = ai_resp.get('insights') or {}
|
||||
if isinstance(insights, dict):
|
||||
tm = insights.get('target_metrics') or insights.get('kpi_targets')
|
||||
if tm:
|
||||
overrides['target_metrics'] = tm
|
||||
except Exception as map_err:
|
||||
logger.warning(f"AI override mapping encountered an issue: {map_err}")
|
||||
|
||||
logger.info(f"AI override mapping produced {len(overrides)} fields: {list(overrides.keys())}")
|
||||
return overrides
|
||||
except Exception as e:
|
||||
logger.error(f"AI override generation failed: {e}")
|
||||
return {}
|
||||
@@ -0,0 +1,187 @@
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
from typing import Any, Dict
|
||||
|
||||
from services.ai_service_manager import AIServiceManager, AIServiceType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CORE_FIELDS = [
|
||||
'business_objectives','target_metrics','content_budget','team_size','implementation_timeline',
|
||||
'market_share','competitive_position','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','preferred_formats',
|
||||
'content_mix','content_frequency','optimal_timing','quality_metrics','editorial_guidelines','brand_voice',
|
||||
'traffic_sources','conversion_rates','content_roi_targets','ab_testing_capabilities'
|
||||
]
|
||||
|
||||
JSON_FIELDS = {
|
||||
'business_objectives', 'target_metrics', 'content_preferences'
|
||||
}
|
||||
ARRAY_FIELDS = {
|
||||
'preferred_formats'
|
||||
}
|
||||
|
||||
class AIStructuredAutofillService:
|
||||
"""Generate the complete 30+ Strategy Builder fields strictly from AI using onboarding context only."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.ai = AIServiceManager()
|
||||
|
||||
def _build_context_summary(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
||||
website = context.get('website_analysis') or {}
|
||||
research = context.get('research_preferences') or {}
|
||||
api_keys = context.get('api_keys_data') or {}
|
||||
session = context.get('onboarding_session') or {}
|
||||
summary = {
|
||||
'website_summary': {
|
||||
'website_url': website.get('website_url'),
|
||||
'industry': website.get('industry'),
|
||||
'content_types': website.get('content_types'),
|
||||
'target_audience': website.get('target_audience'),
|
||||
'performance_metrics': website.get('performance_metrics'),
|
||||
'seo_summary': website.get('seo_analysis')
|
||||
},
|
||||
'research_summary': {
|
||||
'audience_segments': research.get('audience_segments'),
|
||||
'content_preferences': research.get('content_preferences'),
|
||||
'consumption_patterns': research.get('consumption_patterns'),
|
||||
'seasonality': research.get('seasonal_trends')
|
||||
},
|
||||
'api_summary': {
|
||||
'providers': api_keys.get('providers'),
|
||||
'total_keys': api_keys.get('total_keys')
|
||||
},
|
||||
'session_summary': {
|
||||
'business_size': session.get('business_size'),
|
||||
'region': session.get('region')
|
||||
}
|
||||
}
|
||||
try:
|
||||
logger.debug(
|
||||
"AI Structured Autofill: context presence | website=%s research=%s api=%s session=%s",
|
||||
bool(website), bool(research), bool(api_keys), bool(session)
|
||||
)
|
||||
logger.debug(
|
||||
"AI Structured Autofill: website keys=%s research keys=%s",
|
||||
len(list(website.keys())) if hasattr(website, 'keys') else 0,
|
||||
len(list(research.keys())) if hasattr(research, 'keys') else 0,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return summary
|
||||
|
||||
def _build_schema(self) -> Dict[str, Any]:
|
||||
# Build a Gemini SDK-compatible Schema (dict equivalent), not JSON Schema.
|
||||
# Avoid unsupported keys like oneOf/additionalProperties.
|
||||
properties: Dict[str, Any] = {}
|
||||
typed_overrides: Dict[str, Any] = {
|
||||
# Use STRING for complex JSON-bearing fields to avoid OBJECT property constraints
|
||||
'business_objectives': {"type": "STRING"},
|
||||
'target_metrics': {"type": "STRING"},
|
||||
'content_preferences': {"type": "STRING"},
|
||||
# Known arrays
|
||||
'preferred_formats': {"type": "ARRAY", "items": {"type": "STRING"}},
|
||||
# Known selects
|
||||
'content_frequency': {"type": "STRING"},
|
||||
}
|
||||
for key in CORE_FIELDS:
|
||||
properties[key] = typed_overrides.get(key, {"type": "STRING"})
|
||||
schema = {
|
||||
"type": "OBJECT",
|
||||
"properties": properties,
|
||||
# Property ordering can help response consistency per Gemini docs
|
||||
"propertyOrdering": CORE_FIELDS,
|
||||
}
|
||||
logger.debug("AI Structured Autofill: schema built (SDK) with %d properties", len(CORE_FIELDS))
|
||||
return schema
|
||||
|
||||
def _build_prompt(self, context_summary: Dict[str, Any]) -> str:
|
||||
prompt = (
|
||||
"You are a senior content strategy system. Using ONLY the provided context (do not copy raw\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"
|
||||
"If you are uncertain, infer the most reasonable assumption for a small business. Do not leave\n"
|
||||
"fields empty.\n\n"
|
||||
f"CONTEXT:\n{json.dumps(context_summary, indent=2)}\n\n"
|
||||
"FIELDS TO PRODUCE (keys only; values inferred):\n"
|
||||
f"{CORE_FIELDS}\n"
|
||||
)
|
||||
logger.debug("AI Structured Autofill: prompt preview=%d chars", len(prompt))
|
||||
return prompt
|
||||
|
||||
def _normalize_value(self, key: str, value: Any) -> Any:
|
||||
if value is None:
|
||||
return None
|
||||
# Parse JSON-bearing fields if they arrived as JSON strings
|
||||
if key in JSON_FIELDS:
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return json.loads(value)
|
||||
except Exception:
|
||||
# Keep as string if not valid JSON
|
||||
return value
|
||||
return value
|
||||
# Coerce arrays from comma-separated strings where applicable
|
||||
if key in ARRAY_FIELDS:
|
||||
if isinstance(value, str):
|
||||
split = [s.strip() for s in value.split(',') if s.strip()]
|
||||
return split if split else None
|
||||
if isinstance(value, list):
|
||||
return [str(v) for v in value]
|
||||
return None
|
||||
return value
|
||||
|
||||
async def generate_autofill_fields(self, user_id: int, context: Dict[str, Any]) -> Dict[str, Any]:
|
||||
context_summary = self._build_context_summary(context)
|
||||
schema = self._build_schema()
|
||||
prompt = self._build_prompt(context_summary)
|
||||
|
||||
logger.info("AIStructuredAutofillService: generating 30+ fields | user=%s", user_id)
|
||||
logger.debug("AIStructuredAutofillService: properties=%d", len(schema.get('properties', {})))
|
||||
try:
|
||||
result = await self.ai.execute_structured_json_call(
|
||||
service_type=AIServiceType.STRATEGIC_INTELLIGENCE,
|
||||
prompt=prompt,
|
||||
schema=schema
|
||||
)
|
||||
except Exception as e:
|
||||
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):
|
||||
raise ValueError("AI did not return a structured JSON object")
|
||||
|
||||
try:
|
||||
logger.debug("AI structured result keys=%d | sample keys=%s", len(list(result.keys())), list(result.keys())[:8])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Build UI fields map using only non-null normalized values
|
||||
fields: Dict[str, Any] = {}
|
||||
sources: Dict[str, str] = {}
|
||||
non_null_keys = []
|
||||
for key in CORE_FIELDS:
|
||||
raw_value = result.get(key)
|
||||
norm_value = self._normalize_value(key, raw_value)
|
||||
if norm_value is not None and norm_value != "" and norm_value != []:
|
||||
fields[key] = { 'value': norm_value, 'source': 'ai_refresh', 'confidence': 0.8 }
|
||||
sources[key] = 'ai_refresh'
|
||||
non_null_keys.append(key)
|
||||
missing_fields = [k for k in CORE_FIELDS if k not in non_null_keys]
|
||||
|
||||
payload = {
|
||||
'fields': fields,
|
||||
'sources': sources,
|
||||
'meta': {
|
||||
'ai_used': len(non_null_keys) > 0,
|
||||
'ai_overrides_count': len(non_null_keys),
|
||||
'ai_override_fields': non_null_keys,
|
||||
'ai_only': True,
|
||||
'missing_fields': missing_fields
|
||||
}
|
||||
}
|
||||
logger.info("AI structured autofill completed | non_null_fields=%d missing=%d", len(non_null_keys), len(missing_fields))
|
||||
return payload
|
||||
@@ -0,0 +1,79 @@
|
||||
from typing import Any, Dict, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..onboarding.data_integration import OnboardingDataIntegrationService
|
||||
|
||||
# Local module imports (to be created in this batch)
|
||||
from .normalizers.website_normalizer import normalize_website_analysis
|
||||
from .normalizers.research_normalizer import normalize_research_preferences
|
||||
from .normalizers.api_keys_normalizer import normalize_api_keys
|
||||
from .transformer import transform_to_fields
|
||||
from .quality import calculate_quality_scores_from_raw, calculate_confidence_from_raw, calculate_data_freshness
|
||||
from .transparency import build_data_sources_map, build_input_data_points
|
||||
from .schema import validate_output
|
||||
|
||||
|
||||
class AutoFillService:
|
||||
"""Facade for building Content Strategy auto-fill payload."""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
self.integration = OnboardingDataIntegrationService()
|
||||
|
||||
async def get_autofill(self, user_id: int) -> Dict[str, Any]:
|
||||
# 1) Collect raw integration data
|
||||
integrated = await self.integration.process_onboarding_data(user_id, self.db)
|
||||
if not integrated:
|
||||
raise RuntimeError("No onboarding data available for user")
|
||||
|
||||
website_raw = integrated.get('website_analysis', {})
|
||||
research_raw = integrated.get('research_preferences', {})
|
||||
api_raw = integrated.get('api_keys_data', {})
|
||||
session_raw = integrated.get('onboarding_session', {})
|
||||
|
||||
# 2) Normalize raw sources
|
||||
website = await normalize_website_analysis(website_raw)
|
||||
research = await normalize_research_preferences(research_raw)
|
||||
api_keys = await normalize_api_keys(api_raw)
|
||||
|
||||
# 3) Quality/confidence/freshness (computed from raw, but returned as meta)
|
||||
quality_scores = calculate_quality_scores_from_raw({
|
||||
'website_analysis': website_raw,
|
||||
'research_preferences': research_raw,
|
||||
'api_keys_data': api_raw,
|
||||
})
|
||||
confidence_levels = calculate_confidence_from_raw({
|
||||
'website_analysis': website_raw,
|
||||
'research_preferences': research_raw,
|
||||
'api_keys_data': api_raw,
|
||||
})
|
||||
data_freshness = calculate_data_freshness(session_raw)
|
||||
|
||||
# 4) Transform to frontend field map
|
||||
fields = transform_to_fields(
|
||||
website=website,
|
||||
research=research,
|
||||
api_keys=api_keys,
|
||||
session=session_raw,
|
||||
)
|
||||
|
||||
# 5) Transparency maps
|
||||
sources = build_data_sources_map(website, research, api_keys)
|
||||
input_data_points = build_input_data_points(
|
||||
website_raw=website_raw,
|
||||
research_raw=research_raw,
|
||||
api_raw=api_raw,
|
||||
)
|
||||
|
||||
payload = {
|
||||
'fields': fields,
|
||||
'sources': sources,
|
||||
'quality_scores': quality_scores,
|
||||
'confidence_levels': confidence_levels,
|
||||
'data_freshness': data_freshness,
|
||||
'input_data_points': input_data_points,
|
||||
}
|
||||
|
||||
# Validate structure strictly
|
||||
validate_output(payload)
|
||||
return payload
|
||||
@@ -0,0 +1,25 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
async def normalize_api_keys(api_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if not api_data:
|
||||
return {}
|
||||
|
||||
providers = api_data.get('providers', [])
|
||||
|
||||
return {
|
||||
'analytics_data': {
|
||||
'google_analytics': {
|
||||
'connected': 'google_analytics' in providers,
|
||||
'metrics': api_data.get('google_analytics', {}).get('metrics', {})
|
||||
},
|
||||
'google_search_console': {
|
||||
'connected': 'google_search_console' in providers,
|
||||
'metrics': api_data.get('google_search_console', {}).get('metrics', {})
|
||||
}
|
||||
},
|
||||
'social_media_data': api_data.get('social_media_data', {}),
|
||||
'competitor_data': api_data.get('competitor_data', {}),
|
||||
'data_quality': api_data.get('data_quality'),
|
||||
'confidence_level': api_data.get('confidence_level', 0.8),
|
||||
'data_freshness': api_data.get('data_freshness', 0.8)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
async def normalize_research_preferences(research_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if not research_data:
|
||||
return {}
|
||||
|
||||
return {
|
||||
'content_preferences': {
|
||||
'preferred_formats': research_data.get('content_types', []),
|
||||
'content_topics': research_data.get('research_topics', []),
|
||||
'content_style': research_data.get('writing_style', {}).get('tone', []),
|
||||
'content_length': 'Medium (1000-2000 words)',
|
||||
'visual_preferences': ['Infographics', 'Charts', 'Diagrams'],
|
||||
},
|
||||
'audience_intelligence': {
|
||||
'target_audience': research_data.get('target_audience', {}).get('demographics', []),
|
||||
'pain_points': research_data.get('target_audience', {}).get('pain_points', []),
|
||||
'buying_journey': research_data.get('target_audience', {}).get('buying_journey', {}),
|
||||
'consumption_patterns': research_data.get('target_audience', {}).get('consumption_patterns', {}),
|
||||
},
|
||||
'research_goals': {
|
||||
'primary_goals': research_data.get('research_topics', []),
|
||||
'secondary_goals': research_data.get('content_types', []),
|
||||
'success_metrics': ['Website traffic', 'Lead quality', 'Engagement rates'],
|
||||
},
|
||||
'data_quality': research_data.get('data_quality'),
|
||||
'confidence_level': research_data.get('confidence_level', 0.8),
|
||||
'data_freshness': research_data.get('data_freshness', 0.8),
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
async def normalize_website_analysis(website_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if not website_data:
|
||||
return {}
|
||||
|
||||
processed_data = {
|
||||
'website_url': website_data.get('website_url'),
|
||||
'industry': website_data.get('target_audience', {}).get('industry_focus'),
|
||||
'market_position': 'Emerging',
|
||||
'business_size': 'Medium',
|
||||
'target_audience': website_data.get('target_audience', {}).get('demographics'),
|
||||
'content_goals': website_data.get('content_type', {}).get('purpose', []),
|
||||
'performance_metrics': {
|
||||
'traffic': website_data.get('performance_metrics', {}).get('traffic', 10000),
|
||||
'conversion_rate': website_data.get('performance_metrics', {}).get('conversion_rate', 2.5),
|
||||
'bounce_rate': website_data.get('performance_metrics', {}).get('bounce_rate', 50.0),
|
||||
'avg_session_duration': website_data.get('performance_metrics', {}).get('avg_session_duration', 150),
|
||||
'estimated_market_share': website_data.get('performance_metrics', {}).get('estimated_market_share')
|
||||
},
|
||||
'traffic_sources': website_data.get('traffic_sources', {
|
||||
'organic': 70,
|
||||
'social': 20,
|
||||
'direct': 7,
|
||||
'referral': 3
|
||||
}),
|
||||
'content_gaps': website_data.get('style_guidelines', {}).get('content_gaps', []),
|
||||
'topics': website_data.get('content_type', {}).get('primary_type', []),
|
||||
'content_quality_score': website_data.get('content_quality_score', 7.5),
|
||||
'seo_opportunities': website_data.get('style_guidelines', {}).get('seo_opportunities', []),
|
||||
'competitors': website_data.get('competitors', []),
|
||||
'competitive_advantages': website_data.get('style_guidelines', {}).get('advantages', []),
|
||||
'market_gaps': website_data.get('style_guidelines', {}).get('market_gaps', []),
|
||||
'data_quality': website_data.get('data_quality'),
|
||||
'confidence_level': website_data.get('confidence_level', 0.8),
|
||||
'data_freshness': website_data.get('data_freshness', 0.8),
|
||||
'content_budget': website_data.get('content_budget'),
|
||||
'team_size': website_data.get('team_size'),
|
||||
'implementation_timeline': website_data.get('implementation_timeline'),
|
||||
'market_share': website_data.get('market_share'),
|
||||
'target_metrics': website_data.get('target_metrics'),
|
||||
}
|
||||
|
||||
return processed_data
|
||||
@@ -0,0 +1,61 @@
|
||||
from typing import Any, Dict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def calculate_quality_scores_from_raw(data_sources: Dict[str, Any]) -> Dict[str, float]:
|
||||
scores: Dict[str, float] = {}
|
||||
for source, data in data_sources.items():
|
||||
if isinstance(data, dict) and data:
|
||||
total = len(data)
|
||||
non_null = len([v for v in data.values() if v is not None])
|
||||
scores[source] = (non_null / total) * 100 if total else 0.0
|
||||
else:
|
||||
scores[source] = 0.0
|
||||
return scores
|
||||
|
||||
|
||||
def calculate_confidence_from_raw(data_sources: Dict[str, Any]) -> Dict[str, float]:
|
||||
levels: Dict[str, float] = {}
|
||||
if data_sources.get('website_analysis'):
|
||||
levels['website_analysis'] = data_sources['website_analysis'].get('confidence_level', 0.8)
|
||||
if data_sources.get('research_preferences'):
|
||||
levels['research_preferences'] = data_sources['research_preferences'].get('confidence_level', 0.7)
|
||||
if data_sources.get('api_keys_data'):
|
||||
levels['api_keys_data'] = data_sources['api_keys_data'].get('confidence_level', 0.6)
|
||||
return levels
|
||||
|
||||
|
||||
def calculate_data_freshness(onboarding_session: Any) -> Dict[str, Any]:
|
||||
try:
|
||||
updated_at = None
|
||||
if hasattr(onboarding_session, 'updated_at'):
|
||||
updated_at = onboarding_session.updated_at
|
||||
elif isinstance(onboarding_session, dict):
|
||||
updated_at = onboarding_session.get('last_updated') or onboarding_session.get('updated_at')
|
||||
|
||||
if not updated_at:
|
||||
return {'status': 'unknown', 'age_days': 'unknown'}
|
||||
|
||||
if isinstance(updated_at, str):
|
||||
try:
|
||||
updated_at = datetime.fromisoformat(updated_at.replace('Z', '+00:00'))
|
||||
except ValueError:
|
||||
return {'status': 'unknown', 'age_days': 'unknown'}
|
||||
|
||||
age_days = (datetime.utcnow() - updated_at).days
|
||||
if age_days <= 7:
|
||||
status = 'fresh'
|
||||
elif age_days <= 30:
|
||||
status = 'recent'
|
||||
elif age_days <= 90:
|
||||
status = 'aging'
|
||||
else:
|
||||
status = 'stale'
|
||||
|
||||
return {
|
||||
'status': status,
|
||||
'age_days': age_days,
|
||||
'last_updated': updated_at.isoformat() if hasattr(updated_at, 'isoformat') else str(updated_at)
|
||||
}
|
||||
except Exception:
|
||||
return {'status': 'unknown', 'age_days': 'unknown'}
|
||||
@@ -0,0 +1,39 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
REQUIRED_TOP_LEVEL_KEYS = {
|
||||
'fields': dict,
|
||||
'sources': dict,
|
||||
'quality_scores': dict,
|
||||
'confidence_levels': dict,
|
||||
'data_freshness': dict,
|
||||
'input_data_points': dict,
|
||||
}
|
||||
|
||||
|
||||
def validate_output(payload: Dict[str, Any]) -> None:
|
||||
# Top-level keys and types
|
||||
for key, typ in REQUIRED_TOP_LEVEL_KEYS.items():
|
||||
if key not in payload:
|
||||
raise ValueError(f"Autofill payload missing key: {key}")
|
||||
if not isinstance(payload[key], typ):
|
||||
raise ValueError(f"Autofill payload key '{key}' must be {typ.__name__}")
|
||||
|
||||
fields = payload['fields']
|
||||
if not isinstance(fields, dict):
|
||||
raise ValueError("fields must be an object")
|
||||
|
||||
# Allow empty fields, but validate structure when present
|
||||
for field_id, spec in fields.items():
|
||||
if not isinstance(spec, dict):
|
||||
raise ValueError(f"Field '{field_id}' must be an object")
|
||||
for k in ('value', 'source', 'confidence'):
|
||||
if k not in spec:
|
||||
raise ValueError(f"Field '{field_id}' missing '{k}'")
|
||||
if spec['source'] not in ('website_analysis', 'research_preferences', 'api_keys_data', 'onboarding_session'):
|
||||
raise ValueError(f"Field '{field_id}' has invalid source: {spec['source']}")
|
||||
try:
|
||||
c = float(spec['confidence'])
|
||||
except Exception:
|
||||
raise ValueError(f"Field '{field_id}' confidence must be numeric")
|
||||
if c < 0.0 or c > 1.0:
|
||||
raise ValueError(f"Field '{field_id}' confidence must be in [0,1]")
|
||||
@@ -0,0 +1,268 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
def transform_to_fields(*, website: Dict[str, Any], research: Dict[str, Any], api_keys: Dict[str, Any], session: Dict[str, Any]) -> Dict[str, Any]:
|
||||
fields: Dict[str, Any] = {}
|
||||
|
||||
# Business Context
|
||||
if website.get('content_goals'):
|
||||
fields['business_objectives'] = {
|
||||
'value': website.get('content_goals'),
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level')
|
||||
}
|
||||
|
||||
if website.get('target_metrics'):
|
||||
fields['target_metrics'] = {
|
||||
'value': website.get('target_metrics'),
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level')
|
||||
}
|
||||
elif website.get('performance_metrics'):
|
||||
fields['target_metrics'] = {
|
||||
'value': website.get('performance_metrics'),
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level')
|
||||
}
|
||||
|
||||
# content_budget with session fallback
|
||||
if website.get('content_budget') is not None:
|
||||
fields['content_budget'] = {
|
||||
'value': website.get('content_budget'),
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level')
|
||||
}
|
||||
elif isinstance(session, dict) and session.get('budget') is not None:
|
||||
fields['content_budget'] = {
|
||||
'value': session.get('budget'),
|
||||
'source': 'onboarding_session',
|
||||
'confidence': 0.7
|
||||
}
|
||||
|
||||
# team_size with session fallback
|
||||
if website.get('team_size') is not None:
|
||||
fields['team_size'] = {
|
||||
'value': website.get('team_size'),
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level')
|
||||
}
|
||||
elif isinstance(session, dict) and session.get('team_size') is not None:
|
||||
fields['team_size'] = {
|
||||
'value': session.get('team_size'),
|
||||
'source': 'onboarding_session',
|
||||
'confidence': 0.7
|
||||
}
|
||||
|
||||
# implementation_timeline with session fallback
|
||||
if website.get('implementation_timeline'):
|
||||
fields['implementation_timeline'] = {
|
||||
'value': website.get('implementation_timeline'),
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level')
|
||||
}
|
||||
elif isinstance(session, dict) and session.get('timeline'):
|
||||
fields['implementation_timeline'] = {
|
||||
'value': session.get('timeline'),
|
||||
'source': 'onboarding_session',
|
||||
'confidence': 0.7
|
||||
}
|
||||
|
||||
# market_share with derive from performance metrics
|
||||
if website.get('market_share'):
|
||||
fields['market_share'] = {
|
||||
'value': website.get('market_share'),
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level')
|
||||
}
|
||||
elif website.get('performance_metrics'):
|
||||
fields['market_share'] = {
|
||||
'value': website.get('performance_metrics', {}).get('estimated_market_share', None),
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level')
|
||||
}
|
||||
|
||||
# performance metrics
|
||||
fields['performance_metrics'] = {
|
||||
'value': website.get('performance_metrics', {}),
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
# Audience Intelligence
|
||||
audience_research = research.get('audience_intelligence', {})
|
||||
content_prefs = research.get('content_preferences', {})
|
||||
|
||||
fields['content_preferences'] = {
|
||||
'value': content_prefs,
|
||||
'source': 'research_preferences',
|
||||
'confidence': research.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
fields['consumption_patterns'] = {
|
||||
'value': audience_research.get('consumption_patterns', {}),
|
||||
'source': 'research_preferences',
|
||||
'confidence': research.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
fields['audience_pain_points'] = {
|
||||
'value': audience_research.get('pain_points', []),
|
||||
'source': 'research_preferences',
|
||||
'confidence': research.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
fields['buying_journey'] = {
|
||||
'value': audience_research.get('buying_journey', {}),
|
||||
'source': 'research_preferences',
|
||||
'confidence': research.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
fields['seasonal_trends'] = {
|
||||
'value': ['Q1: Planning', 'Q2: Execution', 'Q3: Optimization', 'Q4: Review'],
|
||||
'source': 'research_preferences',
|
||||
'confidence': research.get('confidence_level', 0.7)
|
||||
}
|
||||
|
||||
fields['engagement_metrics'] = {
|
||||
'value': {
|
||||
'avg_session_duration': website.get('performance_metrics', {}).get('avg_session_duration', 180),
|
||||
'bounce_rate': website.get('performance_metrics', {}).get('bounce_rate', 45.5),
|
||||
'pages_per_session': 2.5,
|
||||
},
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
# Competitive Intelligence
|
||||
fields['top_competitors'] = {
|
||||
'value': website.get('competitors', [
|
||||
'Competitor A - Industry Leader',
|
||||
'Competitor B - Emerging Player',
|
||||
'Competitor C - Niche Specialist'
|
||||
]),
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
fields['competitor_content_strategies'] = {
|
||||
'value': ['Educational content', 'Case studies', 'Thought leadership'],
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level', 0.7)
|
||||
}
|
||||
|
||||
fields['market_gaps'] = {
|
||||
'value': website.get('market_gaps', []),
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
fields['industry_trends'] = {
|
||||
'value': ['Digital transformation', 'AI/ML adoption', 'Remote work'],
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
fields['emerging_trends'] = {
|
||||
'value': ['Voice search optimization', 'Video content', 'Interactive content'],
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level', 0.7)
|
||||
}
|
||||
|
||||
# Content Strategy
|
||||
fields['preferred_formats'] = {
|
||||
'value': content_prefs.get('preferred_formats', ['Blog posts', 'Whitepapers', 'Webinars', 'Case studies', 'Videos']),
|
||||
'source': 'research_preferences',
|
||||
'confidence': research.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
fields['content_mix'] = {
|
||||
'value': {
|
||||
'blog_posts': 40,
|
||||
'whitepapers': 20,
|
||||
'webinars': 15,
|
||||
'case_studies': 15,
|
||||
'videos': 10,
|
||||
},
|
||||
'source': 'research_preferences',
|
||||
'confidence': research.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
fields['content_frequency'] = {
|
||||
'value': 'Weekly',
|
||||
'source': 'research_preferences',
|
||||
'confidence': research.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
fields['optimal_timing'] = {
|
||||
'value': {
|
||||
'best_days': ['Tuesday', 'Wednesday', 'Thursday'],
|
||||
'best_times': ['9:00 AM', '1:00 PM', '3:00 PM']
|
||||
},
|
||||
'source': 'research_preferences',
|
||||
'confidence': research.get('confidence_level', 0.7)
|
||||
}
|
||||
|
||||
fields['quality_metrics'] = {
|
||||
'value': {
|
||||
'readability_score': 8.5,
|
||||
'engagement_target': 5.0,
|
||||
'conversion_target': 2.0
|
||||
},
|
||||
'source': 'research_preferences',
|
||||
'confidence': research.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
fields['editorial_guidelines'] = {
|
||||
'value': {
|
||||
'tone': content_prefs.get('content_style', ['Professional', 'Educational']),
|
||||
'length': content_prefs.get('content_length', 'Medium (1000-2000 words)'),
|
||||
'formatting': ['Use headers', 'Include visuals', 'Add CTAs']
|
||||
},
|
||||
'source': 'research_preferences',
|
||||
'confidence': research.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
fields['brand_voice'] = {
|
||||
'value': {
|
||||
'tone': 'Professional yet approachable',
|
||||
'style': 'Educational and authoritative',
|
||||
'personality': 'Expert, helpful, trustworthy'
|
||||
},
|
||||
'source': 'research_preferences',
|
||||
'confidence': research.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
# Performance & Analytics
|
||||
fields['traffic_sources'] = {
|
||||
'value': website.get('traffic_sources', {}),
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
fields['conversion_rates'] = {
|
||||
'value': {
|
||||
'overall': website.get('performance_metrics', {}).get('conversion_rate', 3.2),
|
||||
'blog': 2.5,
|
||||
'landing_pages': 4.0,
|
||||
'email': 5.5,
|
||||
},
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
fields['content_roi_targets'] = {
|
||||
'value': {
|
||||
'target_roi': 300,
|
||||
'cost_per_lead': 50,
|
||||
'lifetime_value': 500,
|
||||
},
|
||||
'source': 'website_analysis',
|
||||
'confidence': website.get('confidence_level', 0.7)
|
||||
}
|
||||
|
||||
fields['ab_testing_capabilities'] = {
|
||||
'value': True,
|
||||
'source': 'api_keys_data',
|
||||
'confidence': api_keys.get('confidence_level', 0.8)
|
||||
}
|
||||
|
||||
return fields
|
||||
@@ -0,0 +1,98 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
def build_data_sources_map(website: Dict[str, Any], research: Dict[str, Any], api_keys: Dict[str, Any]) -> Dict[str, str]:
|
||||
sources: Dict[str, str] = {}
|
||||
|
||||
website_fields = ['business_objectives', 'target_metrics', 'content_budget', 'team_size',
|
||||
'implementation_timeline', 'market_share', 'competitive_position',
|
||||
'performance_metrics', 'engagement_metrics', 'top_competitors',
|
||||
'competitor_content_strategies', 'market_gaps', 'industry_trends',
|
||||
'emerging_trends', 'traffic_sources', 'conversion_rates', 'content_roi_targets']
|
||||
|
||||
research_fields = ['content_preferences', 'consumption_patterns', 'audience_pain_points',
|
||||
'buying_journey', 'seasonal_trends', 'preferred_formats', 'content_mix',
|
||||
'content_frequency', 'optimal_timing', 'quality_metrics', 'editorial_guidelines',
|
||||
'brand_voice']
|
||||
|
||||
api_fields = ['ab_testing_capabilities']
|
||||
|
||||
for f in website_fields:
|
||||
sources[f] = 'website_analysis'
|
||||
for f in research_fields:
|
||||
sources[f] = 'research_preferences'
|
||||
for f in api_fields:
|
||||
sources[f] = 'api_keys_data'
|
||||
|
||||
return sources
|
||||
|
||||
|
||||
def build_input_data_points(*, website_raw: Dict[str, Any], research_raw: Dict[str, Any], api_raw: Dict[str, Any]) -> Dict[str, Any]:
|
||||
input_data_points: Dict[str, Any] = {}
|
||||
|
||||
if website_raw:
|
||||
input_data_points['business_objectives'] = {
|
||||
'website_content': website_raw.get('content_goals', 'Not available'),
|
||||
'meta_description': website_raw.get('meta_description', 'Not available'),
|
||||
'about_page': website_raw.get('about_page_content', 'Not available'),
|
||||
'page_title': website_raw.get('page_title', 'Not available'),
|
||||
'content_analysis': website_raw.get('content_analysis', {})
|
||||
}
|
||||
|
||||
if research_raw:
|
||||
input_data_points['target_metrics'] = {
|
||||
'research_preferences': research_raw.get('target_audience', 'Not available'),
|
||||
'industry_benchmarks': research_raw.get('industry_benchmarks', 'Not available'),
|
||||
'competitor_analysis': research_raw.get('competitor_analysis', 'Not available'),
|
||||
'market_research': research_raw.get('market_research', 'Not available')
|
||||
}
|
||||
|
||||
if research_raw:
|
||||
input_data_points['content_preferences'] = {
|
||||
'user_preferences': research_raw.get('content_types', 'Not available'),
|
||||
'industry_trends': research_raw.get('industry_trends', 'Not available'),
|
||||
'consumption_patterns': research_raw.get('consumption_patterns', 'Not available'),
|
||||
'audience_research': research_raw.get('audience_research', 'Not available')
|
||||
}
|
||||
|
||||
if website_raw or research_raw:
|
||||
input_data_points['preferred_formats'] = {
|
||||
'existing_content': website_raw.get('existing_content_types', 'Not available') if website_raw else 'Not available',
|
||||
'engagement_metrics': website_raw.get('engagement_metrics', 'Not available') if website_raw else 'Not available',
|
||||
'platform_analysis': research_raw.get('platform_preferences', 'Not available') if research_raw else 'Not available',
|
||||
'content_performance': website_raw.get('content_performance', 'Not available') if website_raw else 'Not available'
|
||||
}
|
||||
|
||||
if research_raw:
|
||||
input_data_points['content_frequency'] = {
|
||||
'audience_research': research_raw.get('content_frequency_preferences', 'Not available'),
|
||||
'industry_standards': research_raw.get('industry_frequency', 'Not available'),
|
||||
'competitor_frequency': research_raw.get('competitor_frequency', 'Not available'),
|
||||
'optimal_timing': research_raw.get('optimal_timing', 'Not available')
|
||||
}
|
||||
|
||||
if website_raw:
|
||||
input_data_points['content_budget'] = {
|
||||
'website_analysis': website_raw.get('budget_indicators', 'Not available'),
|
||||
'industry_standards': website_raw.get('industry_budget', 'Not available'),
|
||||
'company_size': website_raw.get('company_size', 'Not available'),
|
||||
'market_position': website_raw.get('market_position', 'Not available')
|
||||
}
|
||||
|
||||
if website_raw:
|
||||
input_data_points['team_size'] = {
|
||||
'company_profile': website_raw.get('company_profile', 'Not available'),
|
||||
'content_volume': website_raw.get('content_volume', 'Not available'),
|
||||
'industry_standards': website_raw.get('industry_team_size', 'Not available'),
|
||||
'budget_constraints': website_raw.get('budget_constraints', 'Not available')
|
||||
}
|
||||
|
||||
if research_raw:
|
||||
input_data_points['implementation_timeline'] = {
|
||||
'project_scope': research_raw.get('project_scope', 'Not available'),
|
||||
'resource_availability': research_raw.get('resource_availability', 'Not available'),
|
||||
'industry_timeline': research_raw.get('industry_timeline', 'Not available'),
|
||||
'complexity_assessment': research_raw.get('complexity_assessment', 'Not available')
|
||||
}
|
||||
|
||||
return input_data_points
|
||||
@@ -1,10 +1,16 @@
|
||||
"""
|
||||
Onboarding Module
|
||||
Onboarding data integration and processing services.
|
||||
Onboarding data integration and processing.
|
||||
"""
|
||||
|
||||
from .data_integration import OnboardingDataIntegrationService
|
||||
from .field_transformation import FieldTransformationService
|
||||
from .data_quality import DataQualityService
|
||||
from .field_transformation import FieldTransformationService
|
||||
from .data_processor import OnboardingDataProcessor
|
||||
|
||||
__all__ = ['OnboardingDataIntegrationService', 'FieldTransformationService', 'DataQualityService']
|
||||
__all__ = [
|
||||
'OnboardingDataIntegrationService',
|
||||
'DataQualityService',
|
||||
'FieldTransformationService',
|
||||
'OnboardingDataProcessor'
|
||||
]
|
||||
@@ -305,19 +305,28 @@ class OnboardingDataIntegrationService:
|
||||
).first()
|
||||
|
||||
if existing_record:
|
||||
existing_record.website_analysis_data = integrated_data.get('website_analysis', {})
|
||||
existing_record.research_preferences_data = integrated_data.get('research_preferences', {})
|
||||
existing_record.api_keys_data = integrated_data.get('api_keys_data', {})
|
||||
# Use legacy columns that are known to exist
|
||||
if hasattr(existing_record, 'website_analysis_data'):
|
||||
existing_record.website_analysis_data = integrated_data.get('website_analysis', {})
|
||||
if hasattr(existing_record, 'research_preferences_data'):
|
||||
existing_record.research_preferences_data = integrated_data.get('research_preferences', {})
|
||||
if hasattr(existing_record, 'api_keys_data'):
|
||||
existing_record.api_keys_data = integrated_data.get('api_keys_data', {})
|
||||
existing_record.updated_at = datetime.utcnow()
|
||||
else:
|
||||
new_record = OnboardingDataIntegration(
|
||||
user_id=user_id,
|
||||
website_analysis_data=integrated_data.get('website_analysis', {}),
|
||||
research_preferences_data=integrated_data.get('research_preferences', {}),
|
||||
api_keys_data=integrated_data.get('api_keys_data', {}),
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
new_kwargs = {
|
||||
'user_id': user_id,
|
||||
'created_at': datetime.utcnow(),
|
||||
'updated_at': datetime.utcnow()
|
||||
}
|
||||
if 'website_analysis' in integrated_data:
|
||||
new_kwargs['website_analysis_data'] = integrated_data.get('website_analysis', {})
|
||||
if 'research_preferences' in integrated_data:
|
||||
new_kwargs['research_preferences_data'] = integrated_data.get('research_preferences', {})
|
||||
if 'api_keys_data' in integrated_data:
|
||||
new_kwargs['api_keys_data'] = integrated_data.get('api_keys_data', {})
|
||||
|
||||
new_record = OnboardingDataIntegration(**new_kwargs)
|
||||
db.add(new_record)
|
||||
|
||||
db.commit()
|
||||
@@ -326,6 +335,8 @@ class OnboardingDataIntegrationService:
|
||||
except Exception as e:
|
||||
logger.error(f"Error storing integrated data for user {user_id}: {str(e)}")
|
||||
db.rollback()
|
||||
# Soft-fail storage: do not break the refresh path
|
||||
return
|
||||
|
||||
def _get_fallback_data(self) -> Dict[str, Any]:
|
||||
"""Get fallback data when processing fails."""
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
"""
|
||||
Onboarding Data Processor
|
||||
Handles processing and transformation of onboarding data for strategic intelligence.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional, Union
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# Import database models
|
||||
from models.onboarding import OnboardingSession, WebsiteAnalysis, ResearchPreferences, APIKey
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OnboardingDataProcessor:
|
||||
"""Processes and transforms onboarding data for strategic intelligence generation."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def process_onboarding_data(self, user_id: int, db: Session) -> Optional[Dict[str, Any]]:
|
||||
"""Process onboarding data for a user and return structured data for strategic intelligence."""
|
||||
try:
|
||||
logger.info(f"Processing onboarding data for user {user_id}")
|
||||
|
||||
# Get onboarding session
|
||||
onboarding_session = db.query(OnboardingSession).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not onboarding_session:
|
||||
logger.warning(f"No onboarding session found for user {user_id}")
|
||||
return None
|
||||
|
||||
# Get website analysis data
|
||||
website_analysis = db.query(WebsiteAnalysis).filter(
|
||||
WebsiteAnalysis.session_id == onboarding_session.id
|
||||
).first()
|
||||
|
||||
# Get research preferences data
|
||||
research_preferences = db.query(ResearchPreferences).filter(
|
||||
ResearchPreferences.session_id == onboarding_session.id
|
||||
).first()
|
||||
|
||||
# Get API keys data
|
||||
api_keys = db.query(APIKey).filter(
|
||||
APIKey.session_id == onboarding_session.id
|
||||
).all()
|
||||
|
||||
# Process each data type
|
||||
processed_data = {
|
||||
'website_analysis': await self._process_website_analysis(website_analysis),
|
||||
'research_preferences': await self._process_research_preferences(research_preferences),
|
||||
'api_keys_data': await self._process_api_keys_data(api_keys),
|
||||
'session_data': self._process_session_data(onboarding_session)
|
||||
}
|
||||
|
||||
# Transform into strategic intelligence format
|
||||
strategic_data = self._transform_to_strategic_format(processed_data)
|
||||
|
||||
logger.info(f"Successfully processed onboarding data for user {user_id}")
|
||||
return strategic_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing onboarding data for user {user_id}: {str(e)}")
|
||||
return None
|
||||
|
||||
async def _process_website_analysis(self, website_analysis: Optional[WebsiteAnalysis]) -> Dict[str, Any]:
|
||||
"""Process website analysis data."""
|
||||
if not website_analysis:
|
||||
return {}
|
||||
|
||||
try:
|
||||
return {
|
||||
'website_url': getattr(website_analysis, 'website_url', ''),
|
||||
'industry': getattr(website_analysis, 'industry', 'Technology'), # Default value if attribute doesn't exist
|
||||
'content_goals': getattr(website_analysis, 'content_goals', []),
|
||||
'performance_metrics': getattr(website_analysis, 'performance_metrics', {}),
|
||||
'traffic_sources': getattr(website_analysis, 'traffic_sources', []),
|
||||
'content_gaps': getattr(website_analysis, 'content_gaps', []),
|
||||
'topics': getattr(website_analysis, 'topics', []),
|
||||
'content_quality_score': getattr(website_analysis, 'content_quality_score', 0),
|
||||
'seo_opportunities': getattr(website_analysis, 'seo_opportunities', []),
|
||||
'competitors': getattr(website_analysis, 'competitors', []),
|
||||
'competitive_advantages': getattr(website_analysis, 'competitive_advantages', []),
|
||||
'market_gaps': getattr(website_analysis, 'market_gaps', []),
|
||||
'last_updated': website_analysis.updated_at.isoformat() if hasattr(website_analysis, 'updated_at') and website_analysis.updated_at else None
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing website analysis: {str(e)}")
|
||||
return {}
|
||||
|
||||
async def _process_research_preferences(self, research_preferences: Optional[ResearchPreferences]) -> Dict[str, Any]:
|
||||
"""Process research preferences data."""
|
||||
if not research_preferences:
|
||||
return {}
|
||||
|
||||
try:
|
||||
return {
|
||||
'content_preferences': {
|
||||
'preferred_formats': research_preferences.content_types,
|
||||
'content_topics': research_preferences.research_topics,
|
||||
'content_style': research_preferences.writing_style.get('tone', []) if research_preferences.writing_style else [],
|
||||
'content_length': research_preferences.content_length,
|
||||
'visual_preferences': research_preferences.visual_preferences
|
||||
},
|
||||
'audience_research': {
|
||||
'target_audience': research_preferences.target_audience.get('demographics', []) if research_preferences.target_audience else [],
|
||||
'audience_pain_points': research_preferences.target_audience.get('pain_points', []) if research_preferences.target_audience else [],
|
||||
'buying_journey': research_preferences.target_audience.get('buying_journey', {}) if research_preferences.target_audience else {},
|
||||
'consumption_patterns': research_preferences.target_audience.get('consumption_patterns', {}) if research_preferences.target_audience else {}
|
||||
},
|
||||
'research_goals': {
|
||||
'primary_goals': research_preferences.research_topics,
|
||||
'secondary_goals': research_preferences.content_types,
|
||||
'success_metrics': research_preferences.success_metrics
|
||||
},
|
||||
'last_updated': research_preferences.updated_at.isoformat() if research_preferences.updated_at else None
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing research preferences: {str(e)}")
|
||||
return {}
|
||||
|
||||
async def _process_api_keys_data(self, api_keys: List[APIKey]) -> Dict[str, Any]:
|
||||
"""Process API keys data."""
|
||||
try:
|
||||
processed_data = {
|
||||
'analytics_data': {},
|
||||
'social_media_data': {},
|
||||
'competitor_data': {},
|
||||
'last_updated': None
|
||||
}
|
||||
|
||||
for api_key in api_keys:
|
||||
if api_key.provider == 'google_analytics':
|
||||
processed_data['analytics_data']['google_analytics'] = {
|
||||
'connected': True,
|
||||
'data_available': True,
|
||||
'metrics': api_key.metrics if api_key.metrics else {}
|
||||
}
|
||||
elif api_key.provider == 'google_search_console':
|
||||
processed_data['analytics_data']['google_search_console'] = {
|
||||
'connected': True,
|
||||
'data_available': True,
|
||||
'metrics': api_key.metrics if api_key.metrics else {}
|
||||
}
|
||||
elif api_key.provider in ['linkedin', 'twitter', 'facebook']:
|
||||
processed_data['social_media_data'][api_key.provider] = {
|
||||
'connected': True,
|
||||
'followers': api_key.metrics.get('followers', 0) if api_key.metrics else 0
|
||||
}
|
||||
elif api_key.provider in ['semrush', 'ahrefs', 'moz']:
|
||||
processed_data['competitor_data'][api_key.provider] = {
|
||||
'connected': True,
|
||||
'competitors_analyzed': api_key.metrics.get('competitors_analyzed', 0) if api_key.metrics else 0
|
||||
}
|
||||
|
||||
# Update last_updated if this key is more recent
|
||||
if api_key.updated_at and (not processed_data['last_updated'] or api_key.updated_at > datetime.fromisoformat(processed_data['last_updated'])):
|
||||
processed_data['last_updated'] = api_key.updated_at.isoformat()
|
||||
|
||||
return processed_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing API keys data: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _process_session_data(self, onboarding_session: OnboardingSession) -> Dict[str, Any]:
|
||||
"""Process onboarding session data."""
|
||||
try:
|
||||
return {
|
||||
'session_id': getattr(onboarding_session, 'id', None),
|
||||
'user_id': getattr(onboarding_session, 'user_id', None),
|
||||
'created_at': onboarding_session.created_at.isoformat() if hasattr(onboarding_session, 'created_at') and onboarding_session.created_at else None,
|
||||
'updated_at': onboarding_session.updated_at.isoformat() if hasattr(onboarding_session, 'updated_at') and onboarding_session.updated_at else None,
|
||||
'completion_status': getattr(onboarding_session, 'completion_status', 'in_progress'),
|
||||
'session_data': getattr(onboarding_session, 'session_data', {}),
|
||||
'progress_percentage': getattr(onboarding_session, 'progress_percentage', 0),
|
||||
'last_activity': getattr(onboarding_session, 'last_activity', None)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing session data: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _transform_to_strategic_format(self, processed_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Transform processed onboarding data into strategic intelligence format."""
|
||||
try:
|
||||
website_data = processed_data.get('website_analysis', {})
|
||||
research_data = processed_data.get('research_preferences', {})
|
||||
api_data = processed_data.get('api_keys_data', {})
|
||||
session_data = processed_data.get('session_data', {})
|
||||
|
||||
# Return data in nested format that field transformation service expects
|
||||
return {
|
||||
'website_analysis': {
|
||||
'content_goals': website_data.get('content_goals', []),
|
||||
'performance_metrics': website_data.get('performance_metrics', {}),
|
||||
'competitors': website_data.get('competitors', []),
|
||||
'content_gaps': website_data.get('content_gaps', []),
|
||||
'industry': website_data.get('industry', 'Technology'),
|
||||
'target_audience': website_data.get('target_audience', {}),
|
||||
'business_type': website_data.get('business_type', 'Technology')
|
||||
},
|
||||
'research_preferences': {
|
||||
'content_types': research_data.get('content_preferences', {}).get('preferred_formats', []),
|
||||
'research_topics': research_data.get('research_topics', []),
|
||||
'performance_tracking': research_data.get('performance_tracking', []),
|
||||
'competitor_analysis': research_data.get('competitor_analysis', []),
|
||||
'target_audience': research_data.get('audience_research', {}).get('target_audience', {}),
|
||||
'industry_focus': research_data.get('industry_focus', []),
|
||||
'trend_analysis': research_data.get('trend_analysis', []),
|
||||
'content_calendar': research_data.get('content_calendar', {})
|
||||
},
|
||||
'onboarding_session': {
|
||||
'session_data': {
|
||||
'budget': session_data.get('budget', 3000),
|
||||
'team_size': session_data.get('team_size', 2),
|
||||
'timeline': session_data.get('timeline', '3 months'),
|
||||
'brand_voice': session_data.get('brand_voice', 'Professional yet approachable')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error transforming to strategic format: {str(e)}")
|
||||
return {}
|
||||
|
||||
def calculate_data_quality_scores(self, processed_data: Dict[str, Any]) -> Dict[str, float]:
|
||||
"""Calculate quality scores for each data source."""
|
||||
scores = {}
|
||||
|
||||
for source, data in processed_data.items():
|
||||
if data and isinstance(data, dict):
|
||||
# Simple scoring based on data completeness
|
||||
total_fields = len(data)
|
||||
present_fields = len([v for v in data.values() if v is not None and v != {}])
|
||||
completeness = present_fields / total_fields if total_fields > 0 else 0.0
|
||||
scores[source] = completeness * 100
|
||||
else:
|
||||
scores[source] = 0.0
|
||||
|
||||
return scores
|
||||
|
||||
def calculate_confidence_levels(self, processed_data: Dict[str, Any]) -> Dict[str, float]:
|
||||
"""Calculate confidence levels for processed data."""
|
||||
confidence_levels = {}
|
||||
|
||||
# Base confidence on data source quality
|
||||
base_confidence = {
|
||||
'website_analysis': 0.8,
|
||||
'research_preferences': 0.7,
|
||||
'api_keys_data': 0.6,
|
||||
'session_data': 0.9
|
||||
}
|
||||
|
||||
for source, data in processed_data.items():
|
||||
if data and isinstance(data, dict):
|
||||
# Adjust confidence based on data completeness
|
||||
quality_score = self.calculate_data_quality_scores({source: data})[source] / 100
|
||||
base_conf = base_confidence.get(source, 0.5)
|
||||
confidence_levels[source] = base_conf * quality_score
|
||||
else:
|
||||
confidence_levels[source] = 0.0
|
||||
|
||||
return confidence_levels
|
||||
|
||||
def calculate_data_freshness(self, session_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Calculate data freshness for onboarding data."""
|
||||
try:
|
||||
updated_at = session_data.get('updated_at')
|
||||
if not updated_at:
|
||||
return {'status': 'unknown', 'age_days': 'unknown'}
|
||||
|
||||
# Convert string to datetime if needed
|
||||
if isinstance(updated_at, str):
|
||||
try:
|
||||
updated_at = datetime.fromisoformat(updated_at.replace('Z', '+00:00'))
|
||||
except ValueError:
|
||||
return {'status': 'unknown', 'age_days': 'unknown'}
|
||||
|
||||
age_days = (datetime.utcnow() - updated_at).days
|
||||
|
||||
if age_days <= 7:
|
||||
status = 'fresh'
|
||||
elif age_days <= 30:
|
||||
status = 'recent'
|
||||
elif age_days <= 90:
|
||||
status = 'aging'
|
||||
else:
|
||||
status = 'stale'
|
||||
|
||||
return {
|
||||
'status': status,
|
||||
'age_days': age_days,
|
||||
'last_updated': updated_at.isoformat() if hasattr(updated_at, 'isoformat') else str(updated_at)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating data freshness: {str(e)}")
|
||||
return {'status': 'unknown', 'age_days': 'unknown'}
|
||||
@@ -92,7 +92,8 @@ class DataQualityService:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error assessing data quality: {str(e)}")
|
||||
return self._get_fallback_quality_assessment()
|
||||
# Raise exception instead of returning fallback data
|
||||
raise Exception(f"Failed to assess data quality: {str(e)}")
|
||||
|
||||
def _assess_website_analysis_quality(self, website_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Assess quality of website analysis data."""
|
||||
@@ -501,22 +502,6 @@ class DataQualityService:
|
||||
logger.error(f"Error identifying quality issues: {str(e)}")
|
||||
return ["Unable to identify issues due to assessment error"]
|
||||
|
||||
def _get_fallback_quality_assessment(self) -> Dict[str, Any]:
|
||||
"""Get fallback quality assessment when assessment fails."""
|
||||
return {
|
||||
'overall_score': 0.0,
|
||||
'completeness': 0.0,
|
||||
'freshness': 0.0,
|
||||
'accuracy': 0.0,
|
||||
'relevance': 0.0,
|
||||
'consistency': 0.0,
|
||||
'confidence': 0.0,
|
||||
'quality_level': 'poor',
|
||||
'recommendations': ['Unable to assess data quality'],
|
||||
'issues': ['Quality assessment failed'],
|
||||
'assessment_timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
def validate_field_data(self, field_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Validate individual field data."""
|
||||
try:
|
||||
|
||||
@@ -147,48 +147,108 @@ class FieldTransformationService:
|
||||
}
|
||||
|
||||
def transform_onboarding_data_to_fields(self, integrated_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Transform integrated onboarding data to strategic input fields."""
|
||||
"""Transform onboarding data to strategic input fields."""
|
||||
try:
|
||||
logger.info("Transforming onboarding data to strategic fields")
|
||||
|
||||
|
||||
transformed_fields = {}
|
||||
data_sources = {}
|
||||
|
||||
for field_id, mapping_config in self.field_mappings.items():
|
||||
try:
|
||||
# Extract data from sources
|
||||
source_data = self._extract_source_data(integrated_data, mapping_config['sources'])
|
||||
|
||||
if source_data:
|
||||
# Apply transformation
|
||||
transformation_method = getattr(self, mapping_config['transformation'])
|
||||
transformed_value = transformation_method(source_data, integrated_data)
|
||||
|
||||
if transformed_value:
|
||||
transformed_fields[field_id] = transformed_value
|
||||
data_sources[field_id] = self._get_data_source_info(mapping_config['sources'], integrated_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error transforming field {field_id}: {str(e)}")
|
||||
continue
|
||||
|
||||
result = {
|
||||
'fields': transformed_fields,
|
||||
'sources': data_sources,
|
||||
'transformation_metadata': {
|
||||
'total_fields_processed': len(self.field_mappings),
|
||||
'successful_transformations': len(transformed_fields),
|
||||
'transformation_timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
transformation_metadata = {
|
||||
'total_fields': 0,
|
||||
'populated_fields': 0,
|
||||
'data_sources_used': [],
|
||||
'confidence_scores': {}
|
||||
}
|
||||
|
||||
logger.info(f"Successfully transformed {len(transformed_fields)} fields from onboarding data")
|
||||
return result
|
||||
|
||||
|
||||
# Process each field mapping
|
||||
for field_name, mapping in self.field_mappings.items():
|
||||
try:
|
||||
sources = mapping.get('sources', [])
|
||||
transformation_method = mapping.get('transformation')
|
||||
|
||||
# Extract source data
|
||||
source_data = self._extract_source_data(integrated_data, sources)
|
||||
|
||||
# Apply transformation if method exists
|
||||
if transformation_method and hasattr(self, transformation_method):
|
||||
transform_func = getattr(self, transformation_method)
|
||||
field_value = transform_func(source_data, integrated_data)
|
||||
else:
|
||||
# Default transformation - use first available source data
|
||||
field_value = self._default_transformation(source_data, field_name)
|
||||
|
||||
# If no value found, provide default based on field type
|
||||
if field_value is None or field_value == "":
|
||||
field_value = self._get_default_value_for_field(field_name)
|
||||
|
||||
if field_value is not None:
|
||||
transformed_fields[field_name] = {
|
||||
'value': field_value,
|
||||
'source': sources[0] if sources else 'default',
|
||||
'confidence': self._calculate_field_confidence(source_data, sources),
|
||||
'auto_populated': True
|
||||
}
|
||||
transformation_metadata['populated_fields'] += 1
|
||||
|
||||
transformation_metadata['total_fields'] += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error transforming field {field_name}: {str(e)}")
|
||||
# Don't provide fallback data - let the error propagate
|
||||
transformation_metadata['total_fields'] += 1
|
||||
|
||||
logger.info(f"Successfully transformed {transformation_metadata['populated_fields']} fields from onboarding data")
|
||||
|
||||
return {
|
||||
'fields': transformed_fields,
|
||||
'sources': self._get_data_source_info(list(self.field_mappings.keys()), integrated_data),
|
||||
'transformation_metadata': transformation_metadata
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error transforming onboarding data to fields: {str(e)}")
|
||||
logger.error(f"Error in transform_onboarding_data_to_fields: {str(e)}")
|
||||
return {'fields': {}, 'sources': {}, 'transformation_metadata': {'error': str(e)}}
|
||||
|
||||
def get_data_sources(self, integrated_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get data sources information for the transformed fields."""
|
||||
try:
|
||||
sources_info = {}
|
||||
for field_name, mapping in self.field_mappings.items():
|
||||
sources = mapping.get('sources', [])
|
||||
sources_info[field_name] = {
|
||||
'sources': sources,
|
||||
'source_count': len(sources),
|
||||
'has_data': any(self._has_source_data(integrated_data, source) for source in sources)
|
||||
}
|
||||
return sources_info
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting data sources: {str(e)}")
|
||||
return {}
|
||||
|
||||
def get_detailed_input_data_points(self, integrated_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get detailed input data points for debugging and analysis."""
|
||||
try:
|
||||
data_points = {}
|
||||
for field_name, mapping in self.field_mappings.items():
|
||||
sources = mapping.get('sources', [])
|
||||
source_data = {}
|
||||
|
||||
for source in sources:
|
||||
source_data[source] = {
|
||||
'exists': self._has_source_data(integrated_data, source),
|
||||
'value': self._get_nested_value(integrated_data, source),
|
||||
'type': type(self._get_nested_value(integrated_data, source)).__name__
|
||||
}
|
||||
|
||||
data_points[field_name] = {
|
||||
'sources': source_data,
|
||||
'transformation_method': mapping.get('transformation'),
|
||||
'has_data': any(source_data[source]['exists'] for source in sources)
|
||||
}
|
||||
return data_points
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting detailed input data points: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _extract_source_data(self, integrated_data: Dict[str, Any], sources: List[str]) -> Dict[str, Any]:
|
||||
"""Extract data from specified sources."""
|
||||
source_data = {}
|
||||
@@ -362,22 +422,34 @@ class FieldTransformationService:
|
||||
return None
|
||||
|
||||
def extract_competitive_position(self, source_data: Dict[str, Any], integrated_data: Dict[str, Any]) -> Optional[str]:
|
||||
"""Extract competitive position from competitor data."""
|
||||
"""Extract and normalize competitive position to one of Leader, Challenger, Niche, Emerging."""
|
||||
try:
|
||||
position_indicators = []
|
||||
text_blobs: list[str] = []
|
||||
|
||||
if 'website_analysis.competitors' in source_data:
|
||||
competitors = source_data['website_analysis.competitors']
|
||||
if competitors:
|
||||
position_indicators.append(f"Competitors: {competitors}")
|
||||
if isinstance(competitors, (str, list, dict)):
|
||||
text_blobs.append(str(competitors))
|
||||
|
||||
if 'research_preferences.competitor_analysis' in source_data:
|
||||
analysis = source_data['research_preferences.competitor_analysis']
|
||||
if analysis:
|
||||
position_indicators.append(f"Analysis: {analysis}")
|
||||
|
||||
return '; '.join(position_indicators) if position_indicators else None
|
||||
if isinstance(analysis, (str, list, dict)):
|
||||
text_blobs.append(str(analysis))
|
||||
|
||||
blob = ' '.join(text_blobs).lower()
|
||||
|
||||
# Simple keyword heuristics
|
||||
if any(kw in blob for kw in ['leader', 'market leader', 'category leader', 'dominant']):
|
||||
return 'Leader'
|
||||
if any(kw in blob for kw in ['challenger', 'fast follower', 'aggressive']):
|
||||
return 'Challenger'
|
||||
if any(kw in blob for kw in ['niche', 'niche player', 'specialized']):
|
||||
return 'Niche'
|
||||
if any(kw in blob for kw in ['emerging', 'new entrant', 'startup', 'growing']):
|
||||
return 'Emerging'
|
||||
|
||||
# No clear signal; let default take over
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting competitive position: {str(e)}")
|
||||
return None
|
||||
@@ -427,6 +499,15 @@ class FieldTransformationService:
|
||||
if research_audience:
|
||||
patterns.append(f"Research Audience: {research_audience}")
|
||||
|
||||
# If we have consumption data as a dict, format it nicely
|
||||
if isinstance(integrated_data.get('consumption_patterns'), dict):
|
||||
consumption_data = integrated_data['consumption_patterns']
|
||||
if isinstance(consumption_data, dict):
|
||||
formatted_patterns = []
|
||||
for platform, percentage in consumption_data.items():
|
||||
formatted_patterns.append(f"{platform.title()}: {percentage}%")
|
||||
patterns.append(', '.join(formatted_patterns))
|
||||
|
||||
return '; '.join(patterns) if patterns else None
|
||||
|
||||
except Exception as e:
|
||||
@@ -465,6 +546,16 @@ class FieldTransformationService:
|
||||
audience = source_data['website_analysis.target_audience']
|
||||
if audience:
|
||||
return f"Journey based on: {audience}"
|
||||
|
||||
# If we have buying journey data as a dict, format it nicely
|
||||
if isinstance(integrated_data.get('buying_journey'), dict):
|
||||
journey_data = integrated_data['buying_journey']
|
||||
if isinstance(journey_data, dict):
|
||||
formatted_journey = []
|
||||
for stage, percentage in journey_data.items():
|
||||
formatted_journey.append(f"{stage.title()}: {percentage}%")
|
||||
return ', '.join(formatted_journey)
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
@@ -599,16 +690,51 @@ class FieldTransformationService:
|
||||
return None
|
||||
|
||||
def extract_preferred_formats(self, source_data: Dict[str, Any], integrated_data: Dict[str, Any]) -> Optional[str]:
|
||||
"""Extract preferred content formats."""
|
||||
"""Extract preferred content formats and normalize to UI option labels array."""
|
||||
try:
|
||||
def to_canonical(label: str) -> Optional[str]:
|
||||
normalized = label.strip().lower()
|
||||
mapping = {
|
||||
'blog': 'Blog Posts',
|
||||
'blog post': 'Blog Posts',
|
||||
'blog posts': 'Blog Posts',
|
||||
'article': 'Blog Posts',
|
||||
'articles': 'Blog Posts',
|
||||
'video': 'Videos',
|
||||
'videos': 'Videos',
|
||||
'infographic': 'Infographics',
|
||||
'infographics': 'Infographics',
|
||||
'webinar': 'Webinars',
|
||||
'webinars': 'Webinars',
|
||||
'podcast': 'Podcasts',
|
||||
'podcasts': 'Podcasts',
|
||||
'case study': 'Case Studies',
|
||||
'case studies': 'Case Studies',
|
||||
'whitepaper': 'Whitepapers',
|
||||
'whitepapers': 'Whitepapers',
|
||||
'social': 'Social Media Posts',
|
||||
'social media': 'Social Media Posts',
|
||||
'social media posts': 'Social Media Posts'
|
||||
}
|
||||
return mapping.get(normalized, None)
|
||||
|
||||
if 'research_preferences.content_types' in source_data:
|
||||
content_types = source_data['research_preferences.content_types']
|
||||
canonical: list[str] = []
|
||||
if isinstance(content_types, list):
|
||||
return ', '.join(content_types)
|
||||
for item in content_types:
|
||||
if isinstance(item, str):
|
||||
canon = to_canonical(item)
|
||||
if canon and canon not in canonical:
|
||||
canonical.append(canon)
|
||||
elif isinstance(content_types, str):
|
||||
return content_types
|
||||
for part in content_types.split(','):
|
||||
canon = to_canonical(part)
|
||||
if canon and canon not in canonical:
|
||||
canonical.append(canon)
|
||||
if canonical:
|
||||
return canonical
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting preferred formats: {str(e)}")
|
||||
return None
|
||||
@@ -654,6 +780,20 @@ class FieldTransformationService:
|
||||
calendar = source_data['research_preferences.content_calendar']
|
||||
if calendar:
|
||||
return str(calendar)
|
||||
|
||||
# If we have optimal timing data as a dict, format it nicely
|
||||
if isinstance(integrated_data.get('optimal_timing'), dict):
|
||||
timing_data = integrated_data['optimal_timing']
|
||||
if isinstance(timing_data, dict):
|
||||
formatted_timing = []
|
||||
if 'best_days' in timing_data:
|
||||
days = timing_data['best_days']
|
||||
if isinstance(days, list):
|
||||
formatted_timing.append(f"Best Days: {', '.join(days)}")
|
||||
if 'best_time' in timing_data:
|
||||
formatted_timing.append(f"Best Time: {timing_data['best_time']}")
|
||||
return ', '.join(formatted_timing)
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
@@ -668,7 +808,19 @@ class FieldTransformationService:
|
||||
if isinstance(metrics, dict):
|
||||
quality_metrics = {k: v for k, v in metrics.items() if 'quality' in k.lower()}
|
||||
if quality_metrics:
|
||||
return ', '.join([f"{k}: {v}" for k, v in quality_metrics.items()])
|
||||
return ', '.join([f"{k.title()}: {v}" for k, v in quality_metrics.items()])
|
||||
elif isinstance(metrics, str):
|
||||
return metrics
|
||||
|
||||
# If we have quality metrics data as a dict, format it nicely
|
||||
if isinstance(integrated_data.get('quality_metrics'), dict):
|
||||
quality_data = integrated_data['quality_metrics']
|
||||
if isinstance(quality_data, dict):
|
||||
formatted_metrics = []
|
||||
for metric, value in quality_data.items():
|
||||
formatted_metrics.append(f"{metric.title()}: {value}")
|
||||
return ', '.join(formatted_metrics)
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
@@ -725,7 +877,9 @@ class FieldTransformationService:
|
||||
if isinstance(metrics, dict):
|
||||
traffic_metrics = {k: v for k, v in metrics.items() if 'traffic' in k.lower()}
|
||||
if traffic_metrics:
|
||||
return ', '.join([f"{k}: {v}" for k, v in traffic_metrics.items()])
|
||||
return ', '.join([f"{k.title()}: {v}%" for k, v in traffic_metrics.items()])
|
||||
elif isinstance(metrics, str):
|
||||
return metrics
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
@@ -740,7 +894,9 @@ class FieldTransformationService:
|
||||
if isinstance(metrics, dict):
|
||||
conversion_metrics = {k: v for k, v in metrics.items() if 'conversion' in k.lower()}
|
||||
if conversion_metrics:
|
||||
return ', '.join([f"{k}: {v}" for k, v in conversion_metrics.items()])
|
||||
return ', '.join([f"{k.title()}: {v}%" for k, v in conversion_metrics.items()])
|
||||
elif isinstance(metrics, str):
|
||||
return metrics
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
@@ -770,21 +926,135 @@ class FieldTransformationService:
|
||||
logger.error(f"Error extracting ROI targets: {str(e)}")
|
||||
return None
|
||||
|
||||
def extract_ab_testing_capabilities(self, source_data: Dict[str, Any], integrated_data: Dict[str, Any]) -> Optional[str]:
|
||||
def extract_ab_testing_capabilities(self, source_data: Dict[str, Any], integrated_data: Dict[str, Any]) -> Optional[bool]:
|
||||
"""Extract A/B testing capabilities from team size."""
|
||||
try:
|
||||
if 'onboarding_session.session_data.team_size' in source_data:
|
||||
team_size = source_data['onboarding_session.session_data.team_size']
|
||||
if team_size:
|
||||
# Simple logic based on team size
|
||||
if int(team_size) > 5:
|
||||
return "Advanced A/B testing capabilities"
|
||||
elif int(team_size) > 2:
|
||||
return "Basic A/B testing capabilities"
|
||||
else:
|
||||
return "Limited A/B testing capabilities"
|
||||
return None
|
||||
# Return boolean based on team size
|
||||
team_size_int = int(team_size) if isinstance(team_size, (str, int, float)) else 1
|
||||
return team_size_int > 2 # True if team size > 2, False otherwise
|
||||
|
||||
# Default to False if no team size data
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting A/B testing capabilities: {str(e)}")
|
||||
return False
|
||||
|
||||
def _get_default_value_for_field(self, field_name: str) -> Any:
|
||||
"""Get default value for a field when no data is available."""
|
||||
# Provide sensible defaults for required fields
|
||||
default_values = {
|
||||
'business_objectives': 'Lead Generation, Brand Awareness',
|
||||
'target_metrics': 'Traffic Growth: 30%, Engagement Rate: 5%, Conversion Rate: 2%',
|
||||
'content_budget': 1000,
|
||||
'team_size': 1,
|
||||
'implementation_timeline': '3 months',
|
||||
'market_share': 'Small but growing',
|
||||
'competitive_position': 'Niche',
|
||||
'performance_metrics': 'Current Traffic: 1000, Current Engagement: 3%',
|
||||
'content_preferences': 'Blog posts, Social media content',
|
||||
'consumption_patterns': 'Mobile: 60%, Desktop: 40%',
|
||||
'audience_pain_points': 'Time constraints, Content quality',
|
||||
'buying_journey': 'Awareness: 40%, Consideration: 35%, Decision: 25%',
|
||||
'seasonal_trends': 'Q4 peak, Summer slowdown',
|
||||
'engagement_metrics': 'Likes: 100, Shares: 20, Comments: 15',
|
||||
'top_competitors': 'Competitor A, Competitor B',
|
||||
'competitor_content_strategies': 'Blog-focused, Video-heavy',
|
||||
'market_gaps': 'Underserved niche, Content gap',
|
||||
'industry_trends': 'AI integration, Video content',
|
||||
'emerging_trends': 'Voice search, Interactive content',
|
||||
'preferred_formats': ['Blog Posts', 'Videos', 'Infographics'],
|
||||
'content_mix': 'Educational: 40%, Entertaining: 30%, Promotional: 30%',
|
||||
'content_frequency': 'Weekly',
|
||||
'optimal_timing': 'Best Days: Tuesday, Thursday, Best Time: 10 AM',
|
||||
'quality_metrics': 'Readability: 8, Engagement: 7, SEO Score: 6',
|
||||
'editorial_guidelines': 'Professional tone, Clear structure',
|
||||
'brand_voice': 'Professional yet approachable',
|
||||
'traffic_sources': 'Organic: 60%, Social: 25%, Direct: 15%',
|
||||
'conversion_rates': 'Overall: 2%, Blog: 3%, Landing Pages: 5%',
|
||||
'content_roi_targets': 'Target ROI: 300%, Break Even: 6 months',
|
||||
'ab_testing_capabilities': False
|
||||
}
|
||||
|
||||
return default_values.get(field_name, None)
|
||||
|
||||
def _default_transformation(self, source_data: Dict[str, Any], field_name: str) -> Any:
|
||||
"""Default transformation when no specific method is available."""
|
||||
try:
|
||||
# Try to find any non-empty value in source data
|
||||
for key, value in source_data.items():
|
||||
if value is not None and value != "":
|
||||
# For budget and team_size, try to convert to number
|
||||
if field_name in ['content_budget', 'team_size'] and isinstance(value, (str, int, float)):
|
||||
try:
|
||||
return int(value) if field_name == 'team_size' else float(value)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
# For other fields, return the first non-empty value
|
||||
return value
|
||||
|
||||
# If no value found, return None
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error in default transformation for {field_name}: {str(e)}")
|
||||
return None
|
||||
|
||||
def _calculate_field_confidence(self, source_data: Dict[str, Any], sources: List[str]) -> float:
|
||||
"""Calculate confidence score for a field based on data quality and source availability."""
|
||||
try:
|
||||
if not source_data:
|
||||
return 0.3 # Low confidence when no data
|
||||
|
||||
# Check data quality indicators
|
||||
data_quality_score = 0.0
|
||||
total_indicators = 0
|
||||
|
||||
# Check if data is not empty
|
||||
for key, value in source_data.items():
|
||||
if value is not None and value != "":
|
||||
data_quality_score += 1.0
|
||||
total_indicators += 1
|
||||
|
||||
# Check source availability
|
||||
source_availability = len([s for s in sources if self._has_source_data(source_data, s)]) / max(len(sources), 1)
|
||||
|
||||
# Calculate final confidence
|
||||
if total_indicators > 0:
|
||||
data_quality = data_quality_score / total_indicators
|
||||
confidence = (data_quality + source_availability) / 2
|
||||
return min(confidence, 1.0) # Cap at 1.0
|
||||
else:
|
||||
return 0.3 # Default low confidence
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating field confidence: {str(e)}")
|
||||
return 0.3 # Default low confidence
|
||||
|
||||
def _has_source_data(self, integrated_data: Dict[str, Any], source_path: str) -> bool:
|
||||
"""Check if source data exists in integrated data."""
|
||||
try:
|
||||
value = self._get_nested_value(integrated_data, source_path)
|
||||
return value is not None and value != ""
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking source data for {source_path}: {str(e)}")
|
||||
return False
|
||||
|
||||
def _get_nested_value(self, data: Dict[str, Any], path: str) -> Any:
|
||||
"""Get nested value from dictionary using dot notation."""
|
||||
try:
|
||||
keys = path.split('.')
|
||||
value = data
|
||||
|
||||
for key in keys:
|
||||
if isinstance(value, dict) and key in value:
|
||||
value = value[key]
|
||||
else:
|
||||
return None
|
||||
|
||||
return value
|
||||
except Exception as e:
|
||||
logger.debug(f"Error getting nested value for {path}: {str(e)}")
|
||||
return None
|
||||
@@ -500,4 +500,95 @@ class HealthMonitoringService:
|
||||
await asyncio.sleep(60) # Wait 1 minute before retrying
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting continuous monitoring: {str(e)}")
|
||||
logger.error(f"Error starting continuous monitoring: {str(e)}")
|
||||
|
||||
async def get_performance_metrics(self) -> Dict[str, Any]:
|
||||
"""Get comprehensive performance metrics."""
|
||||
try:
|
||||
# Calculate average response times
|
||||
response_times = self.performance_metrics.get('response_times', [])
|
||||
if response_times:
|
||||
avg_response_time = sum(rt['response_time'] for rt in response_times) / len(response_times)
|
||||
max_response_time = max(rt['response_time'] for rt in response_times)
|
||||
min_response_time = min(rt['response_time'] for rt in response_times)
|
||||
else:
|
||||
avg_response_time = max_response_time = min_response_time = 0.0
|
||||
|
||||
# Calculate cache hit rates
|
||||
cache_hit_rates = {}
|
||||
for cache_name, stats in self.cache_stats.items():
|
||||
total_requests = stats['hits'] + stats['misses']
|
||||
hit_rate = (stats['hits'] / total_requests * 100) if total_requests > 0 else 0.0
|
||||
cache_hit_rates[cache_name] = {
|
||||
'hit_rate': hit_rate,
|
||||
'total_requests': total_requests,
|
||||
'cache_size': stats['size']
|
||||
}
|
||||
|
||||
# Calculate error rates (placeholder - implement actual error tracking)
|
||||
error_rates = {
|
||||
'ai_analysis_errors': 0.05, # 5% error rate
|
||||
'onboarding_data_errors': 0.02, # 2% error rate
|
||||
'strategy_creation_errors': 0.01 # 1% error rate
|
||||
}
|
||||
|
||||
# Calculate throughput metrics
|
||||
throughput_metrics = {
|
||||
'requests_per_minute': len(response_times) / 60 if response_times else 0,
|
||||
'successful_requests': len([rt for rt in response_times if rt.get('performance_status') != 'error']),
|
||||
'failed_requests': len([rt for rt in response_times if rt.get('performance_status') == 'error'])
|
||||
}
|
||||
|
||||
return {
|
||||
'response_time_metrics': {
|
||||
'average_response_time': avg_response_time,
|
||||
'max_response_time': max_response_time,
|
||||
'min_response_time': min_response_time,
|
||||
'response_time_threshold': 5.0
|
||||
},
|
||||
'cache_metrics': cache_hit_rates,
|
||||
'error_metrics': error_rates,
|
||||
'throughput_metrics': throughput_metrics,
|
||||
'system_health': {
|
||||
'cache_utilization': 0.7, # Simplified
|
||||
'memory_usage': len(response_times) / 1000, # Simplified memory usage
|
||||
'overall_performance': 'optimal' if avg_response_time <= 2.0 else 'acceptable' if avg_response_time <= 5.0 else 'needs_optimization'
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting performance metrics: {str(e)}")
|
||||
return {}
|
||||
|
||||
async def monitor_system_health(self) -> Dict[str, Any]:
|
||||
"""Monitor system health and performance."""
|
||||
try:
|
||||
# Get current performance metrics
|
||||
performance_metrics = await self.get_performance_metrics()
|
||||
|
||||
# Health checks
|
||||
health_checks = {
|
||||
'database_connectivity': await self._check_database_health(None), # Will be passed in actual usage
|
||||
'cache_functionality': {'status': 'healthy', 'utilization': 0.7},
|
||||
'ai_service_availability': {'status': 'healthy', 'response_time': 2.5, 'availability': 0.99},
|
||||
'response_time_health': {'status': 'healthy', 'average_response_time': 1.5, 'threshold': 5.0},
|
||||
'error_rate_health': {'status': 'healthy', 'error_rate': 0.02, 'threshold': 0.05}
|
||||
}
|
||||
|
||||
# Overall health status
|
||||
overall_health = 'healthy'
|
||||
if any(check.get('status') == 'critical' for check in health_checks.values()):
|
||||
overall_health = 'critical'
|
||||
elif any(check.get('status') == 'warning' for check in health_checks.values()):
|
||||
overall_health = 'warning'
|
||||
|
||||
return {
|
||||
'overall_health': overall_health,
|
||||
'health_checks': health_checks,
|
||||
'performance_metrics': performance_metrics,
|
||||
'recommendations': ['System is performing well', 'Monitor cache utilization']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error monitoring system health: {str(e)}")
|
||||
return {'overall_health': 'unknown', 'error': str(e)}
|
||||
@@ -12,6 +12,7 @@ from sqlalchemy import and_, or_
|
||||
|
||||
# Import database models
|
||||
from models.enhanced_strategy_models import EnhancedContentStrategy, EnhancedAIAnalysisResult, OnboardingDataIntegration
|
||||
from models.enhanced_strategy_models import ContentStrategyAutofillInsights
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -229,4 +230,50 @@ class EnhancedStrategyDBService:
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting strategy export data for strategy {strategy_id}: {str(e)}")
|
||||
return None
|
||||
|
||||
async def save_autofill_insights(self, *, strategy_id: int, user_id: int, payload: Dict[str, Any]) -> Optional[ContentStrategyAutofillInsights]:
|
||||
"""Persist accepted auto-fill inputs used to create a strategy."""
|
||||
try:
|
||||
record = ContentStrategyAutofillInsights(
|
||||
strategy_id=strategy_id,
|
||||
user_id=user_id,
|
||||
accepted_fields=payload.get('accepted_fields') or {},
|
||||
sources=payload.get('sources') or {},
|
||||
input_data_points=payload.get('input_data_points') or {},
|
||||
quality_scores=payload.get('quality_scores') or {},
|
||||
confidence_levels=payload.get('confidence_levels') or {},
|
||||
data_freshness=payload.get('data_freshness') or {}
|
||||
)
|
||||
self.db.add(record)
|
||||
self.db.commit()
|
||||
self.db.refresh(record)
|
||||
return record
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving autofill insights for strategy {strategy_id}: {str(e)}")
|
||||
self.db.rollback()
|
||||
return None
|
||||
|
||||
async def get_latest_autofill_insights(self, strategy_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch the most recent accepted auto-fill snapshot for a strategy."""
|
||||
try:
|
||||
record = self.db.query(ContentStrategyAutofillInsights).filter(
|
||||
ContentStrategyAutofillInsights.strategy_id == strategy_id
|
||||
).order_by(ContentStrategyAutofillInsights.created_at.desc()).first()
|
||||
if not record:
|
||||
return None
|
||||
return {
|
||||
'id': record.id,
|
||||
'strategy_id': record.strategy_id,
|
||||
'user_id': record.user_id,
|
||||
'accepted_fields': record.accepted_fields,
|
||||
'sources': record.sources,
|
||||
'input_data_points': record.input_data_points,
|
||||
'quality_scores': record.quality_scores,
|
||||
'confidence_levels': record.confidence_levels,
|
||||
'data_freshness': record.data_freshness,
|
||||
'created_at': record.created_at.isoformat() if record.created_at else None
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching latest autofill insights for strategy {strategy_id}: {str(e)}")
|
||||
return None
|
||||
File diff suppressed because it is too large
Load Diff
@@ -80,6 +80,9 @@ class EnhancedContentStrategy(Base):
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
completion_percentage = Column(Float, default=0.0) # Track input completion
|
||||
data_source_transparency = Column(JSON, nullable=True) # Track data sources for auto-population
|
||||
|
||||
# Relationships
|
||||
autofill_insights = relationship("ContentStrategyAutofillInsights", back_populates="strategy", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<EnhancedContentStrategy(id={self.id}, name='{self.name}', industry='{self.industry}')>"
|
||||
@@ -238,17 +241,17 @@ class OnboardingDataIntegration(Base):
|
||||
user_id = Column(Integer, nullable=False)
|
||||
strategy_id = Column(Integer, ForeignKey("enhanced_content_strategies.id"), nullable=True)
|
||||
|
||||
# Onboarding data sources
|
||||
# Legacy onboarding storage fields (match existing DB schema)
|
||||
website_analysis_data = Column(JSON, nullable=True) # Data from website analysis
|
||||
research_preferences_data = Column(JSON, nullable=True) # Data from research preferences
|
||||
api_keys_data = Column(JSON, nullable=True) # API configuration data
|
||||
|
||||
# Integration mapping
|
||||
# Integration mapping and user edits
|
||||
field_mappings = Column(JSON, nullable=True) # Mapping of onboarding fields to strategy fields
|
||||
auto_populated_fields = Column(JSON, nullable=True) # Fields auto-populated from onboarding
|
||||
user_overrides = Column(JSON, nullable=True) # Fields manually overridden by user
|
||||
|
||||
# Data quality and confidence
|
||||
# Data quality and transparency
|
||||
data_quality_scores = Column(JSON, nullable=True) # Quality scores for each data source
|
||||
confidence_levels = Column(JSON, nullable=True) # Confidence levels for auto-populated data
|
||||
data_freshness = Column(JSON, nullable=True) # How recent the onboarding data is
|
||||
@@ -256,12 +259,9 @@ class OnboardingDataIntegration(Base):
|
||||
# Metadata
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<OnboardingDataIntegration(id={self.id}, user_id={self.user_id}, strategy_id={self.strategy_id})>"
|
||||
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert model to dictionary."""
|
||||
"""Convert model to dictionary (legacy fields)."""
|
||||
return {
|
||||
'id': self.id,
|
||||
'user_id': self.user_id,
|
||||
@@ -277,4 +277,25 @@ class OnboardingDataIntegration(Base):
|
||||
'data_freshness': self.data_freshness,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
}
|
||||
|
||||
# New model to persist accepted auto-fill inputs used to create a strategy
|
||||
class ContentStrategyAutofillInsights(Base):
|
||||
__tablename__ = "content_strategy_autofill_insights"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
strategy_id = Column(Integer, ForeignKey("enhanced_content_strategies.id"), nullable=False)
|
||||
user_id = Column(Integer, nullable=False)
|
||||
|
||||
# Full snapshot of accepted inputs and transparency at time of strategy creation/confirmation
|
||||
accepted_fields = Column(JSON, nullable=False)
|
||||
sources = Column(JSON, nullable=True)
|
||||
input_data_points = Column(JSON, nullable=True)
|
||||
quality_scores = Column(JSON, nullable=True)
|
||||
confidence_levels = Column(JSON, nullable=True)
|
||||
data_freshness = Column(JSON, nullable=True)
|
||||
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Relationship back to strategy
|
||||
strategy = relationship("EnhancedContentStrategy", back_populates="autofill_insights")
|
||||
@@ -13,7 +13,13 @@ from enum import Enum
|
||||
|
||||
# Import AI providers
|
||||
from llm_providers.main_text_generation import llm_text_gen
|
||||
from llm_providers.gemini_provider import gemini_structured_json_response
|
||||
# Prefer the extended gemini provider if available; fallback to base
|
||||
try:
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response as _gemini_fn
|
||||
_GEMINI_EXTENDED = True
|
||||
except Exception:
|
||||
from llm_providers.gemini_provider import gemini_structured_json_response as _gemini_fn
|
||||
_GEMINI_EXTENDED = False
|
||||
|
||||
class AIServiceType(Enum):
|
||||
"""AI service types for monitoring."""
|
||||
@@ -54,14 +60,16 @@ class AIServiceManager:
|
||||
def _load_ai_configuration(self) -> Dict[str, Any]:
|
||||
"""Load AI configuration settings."""
|
||||
return {
|
||||
'max_retries': 3,
|
||||
'timeout_seconds': 30,
|
||||
'temperature': 0.7,
|
||||
'max_tokens': 2048,
|
||||
'max_retries': 2, # Reduced from 3
|
||||
'timeout_seconds': 45, # increased from 15 to accommodate structured 30+ fields
|
||||
'temperature': 0.3, # more deterministic for schema-constrained JSON
|
||||
'top_p': 0.9,
|
||||
'top_k': 40,
|
||||
'max_tokens': 2048, # increased from 1024 for larger structured outputs
|
||||
'enable_caching': True,
|
||||
'cache_duration_minutes': 60,
|
||||
'performance_monitoring': True,
|
||||
'fallback_enabled': True
|
||||
'fallback_enabled': False # Disabled fallback to prevent false positives
|
||||
}
|
||||
|
||||
def _load_centralized_prompts(self) -> Dict[str, str]:
|
||||
@@ -448,47 +456,120 @@ Format as structured JSON with detailed assessment and optimization guidance.
|
||||
|
||||
try:
|
||||
logger.info(f"🤖 Executing AI call for {service_type.value}")
|
||||
logger.debug(f"Using gemini provider extended={_GEMINI_EXTENDED}")
|
||||
|
||||
# Execute AI call with timeout
|
||||
# Execute AI call with timeout (run sync provider in a thread)
|
||||
response = await asyncio.wait_for(
|
||||
gemini_structured_json_response(
|
||||
prompt=prompt,
|
||||
schema=schema,
|
||||
temperature=self.config['temperature'],
|
||||
max_tokens=self.config['max_tokens']
|
||||
asyncio.to_thread(
|
||||
self._call_gemini_structured,
|
||||
prompt,
|
||||
schema,
|
||||
),
|
||||
timeout=self.config['timeout_seconds']
|
||||
)
|
||||
|
||||
# Parse response
|
||||
result = json.loads(response)
|
||||
if isinstance(response, dict):
|
||||
result = response
|
||||
elif isinstance(response, str):
|
||||
try:
|
||||
result = json.loads(response)
|
||||
except json.JSONDecodeError:
|
||||
# Return raw string if not valid JSON
|
||||
result = {"raw_response": response}
|
||||
else:
|
||||
# Fallback to string conversion
|
||||
result = {"raw_response": str(response)}
|
||||
|
||||
# Treat provider-reported errors or empty results as failures
|
||||
if isinstance(result, dict) and ('error' in result or not result):
|
||||
error_message = result.get('error', 'Empty AI response') if isinstance(result, dict) else 'Empty AI response'
|
||||
# record metrics and raise
|
||||
response_time = (datetime.utcnow() - start_time).total_seconds()
|
||||
metrics = AIServiceMetrics(
|
||||
service_type=service_type,
|
||||
response_time=response_time,
|
||||
success=False,
|
||||
error_message=error_message
|
||||
)
|
||||
self.metrics.append(metrics)
|
||||
raise Exception(error_message)
|
||||
|
||||
success = True
|
||||
logger.info(f"✅ AI call for {service_type.value} completed successfully")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
error_message = f"AI call timeout for {service_type.value}"
|
||||
logger.error(error_message)
|
||||
# record metrics and raise
|
||||
response_time = (datetime.utcnow() - start_time).total_seconds()
|
||||
metrics = AIServiceMetrics(
|
||||
service_type=service_type,
|
||||
response_time=response_time,
|
||||
success=False,
|
||||
error_message=error_message
|
||||
)
|
||||
self.metrics.append(metrics)
|
||||
raise Exception(error_message)
|
||||
except json.JSONDecodeError as e:
|
||||
error_message = f"JSON decode error for {service_type.value}: {str(e)}"
|
||||
logger.error(error_message)
|
||||
response_time = (datetime.utcnow() - start_time).total_seconds()
|
||||
metrics = AIServiceMetrics(
|
||||
service_type=service_type,
|
||||
response_time=response_time,
|
||||
success=False,
|
||||
error_message=error_message
|
||||
)
|
||||
self.metrics.append(metrics)
|
||||
raise Exception(error_message)
|
||||
except Exception as e:
|
||||
error_message = f"AI call error for {service_type.value}: {str(e)}"
|
||||
logger.error(error_message)
|
||||
response_time = (datetime.utcnow() - start_time).total_seconds()
|
||||
metrics = AIServiceMetrics(
|
||||
service_type=service_type,
|
||||
response_time=response_time,
|
||||
success=False,
|
||||
error_message=error_message
|
||||
)
|
||||
self.metrics.append(metrics)
|
||||
raise
|
||||
|
||||
# Calculate response time
|
||||
# Calculate response time and record metrics for successful calls
|
||||
response_time = (datetime.utcnow() - start_time).total_seconds()
|
||||
|
||||
# Record metrics
|
||||
metrics = AIServiceMetrics(
|
||||
service_type=service_type,
|
||||
response_time=response_time,
|
||||
success=success,
|
||||
error_message=error_message
|
||||
error_message=None
|
||||
)
|
||||
self.metrics.append(metrics)
|
||||
|
||||
return result
|
||||
|
||||
def _call_gemini_structured(self, prompt: str, schema: Dict[str, Any]):
|
||||
"""Call gemini structured JSON with flexible signature support.
|
||||
Tries extended signature first; falls back to minimal signature to avoid TypeError.
|
||||
"""
|
||||
try:
|
||||
# Attempt extended signature (temperature/top_p/top_k/max_tokens/system_prompt)
|
||||
return _gemini_fn(
|
||||
prompt,
|
||||
schema,
|
||||
self.config['temperature'],
|
||||
self.config['top_p'],
|
||||
self.config.get('top_k', 40),
|
||||
self.config['max_tokens'],
|
||||
None
|
||||
)
|
||||
except TypeError:
|
||||
logger.debug("Falling back to base gemini provider signature (prompt, schema)")
|
||||
return _gemini_fn(prompt, schema)
|
||||
|
||||
async def execute_structured_json_call(self, service_type: AIServiceType, prompt: str, schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Public wrapper to execute a structured JSON AI call with a provided schema."""
|
||||
return await self._execute_ai_call(service_type, prompt, schema)
|
||||
|
||||
async def generate_content_gap_analysis(self, analysis_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate content gap analysis using centralized AI service.
|
||||
@@ -520,11 +601,11 @@ Format as structured JSON with detailed assessment and optimization guidance.
|
||||
self.schemas['content_gap_analysis']
|
||||
)
|
||||
|
||||
return result if result else self._get_fallback_content_gap_analysis()
|
||||
return result if result else {}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in content gap analysis: {str(e)}")
|
||||
return self._get_fallback_content_gap_analysis()
|
||||
raise Exception(f"Failed to generate content gap analysis: {str(e)}")
|
||||
|
||||
async def generate_market_position_analysis(self, market_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -553,11 +634,11 @@ Format as structured JSON with detailed assessment and optimization guidance.
|
||||
self.schemas['market_position_analysis']
|
||||
)
|
||||
|
||||
return result if result else self._get_fallback_market_position_analysis()
|
||||
return result if result else {}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in market position analysis: {str(e)}")
|
||||
return self._get_fallback_market_position_analysis()
|
||||
raise Exception(f"Failed to generate market position analysis: {str(e)}")
|
||||
|
||||
async def generate_keyword_analysis(self, keyword_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -586,11 +667,11 @@ Format as structured JSON with detailed assessment and optimization guidance.
|
||||
self.schemas['keyword_analysis']
|
||||
)
|
||||
|
||||
return result if result else self._get_fallback_keyword_analysis()
|
||||
return result if result else {}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in keyword analysis: {str(e)}")
|
||||
return self._get_fallback_keyword_analysis()
|
||||
raise Exception(f"Failed to generate keyword analysis: {str(e)}")
|
||||
|
||||
async def generate_performance_prediction(self, content_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -618,11 +699,11 @@ Format as structured JSON with detailed assessment and optimization guidance.
|
||||
self.schemas['performance_prediction']
|
||||
)
|
||||
|
||||
return result if result else self._get_fallback_performance_prediction()
|
||||
return result if result else {}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in performance prediction: {str(e)}")
|
||||
return self._get_fallback_performance_prediction()
|
||||
raise Exception(f"Failed to generate performance prediction: {str(e)}")
|
||||
|
||||
async def generate_strategic_intelligence(self, analysis_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -651,11 +732,11 @@ Format as structured JSON with detailed assessment and optimization guidance.
|
||||
self.schemas['strategic_intelligence']
|
||||
)
|
||||
|
||||
return result if result else self._get_fallback_strategic_intelligence()
|
||||
return result if result else {}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in strategic intelligence: {str(e)}")
|
||||
return self._get_fallback_strategic_intelligence()
|
||||
raise Exception(f"Failed to generate strategic intelligence: {str(e)}")
|
||||
|
||||
async def generate_content_quality_assessment(self, content_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -684,11 +765,11 @@ Format as structured JSON with detailed assessment and optimization guidance.
|
||||
self.schemas['content_quality_assessment']
|
||||
)
|
||||
|
||||
return result if result else self._get_fallback_content_quality_assessment()
|
||||
return result if result else {}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in content quality assessment: {str(e)}")
|
||||
return self._get_fallback_content_quality_assessment()
|
||||
raise Exception(f"Failed to generate content quality assessment: {str(e)}")
|
||||
|
||||
async def generate_content_schedule(self, prompt: str) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -733,109 +814,6 @@ Format as structured JSON with detailed assessment and optimization guidance.
|
||||
logger.error(f"Error generating content schedule: {str(e)}")
|
||||
return {"schedule": []}
|
||||
|
||||
# Fallback methods
|
||||
def _get_fallback_content_gap_analysis(self) -> Dict[str, Any]:
|
||||
"""Fallback content gap analysis."""
|
||||
return {
|
||||
'strategic_insights': [
|
||||
{
|
||||
'type': 'content_strategy',
|
||||
'insight': 'Focus on educational content to build authority',
|
||||
'confidence': 0.85,
|
||||
'priority': 'high',
|
||||
'estimated_impact': 'Authority building',
|
||||
'implementation_time': '3-6 months',
|
||||
'risk_level': 'low'
|
||||
}
|
||||
],
|
||||
'content_recommendations': [
|
||||
{
|
||||
'type': 'content_creation',
|
||||
'recommendation': 'Create comprehensive guides for high-opportunity keywords',
|
||||
'priority': 'high',
|
||||
'estimated_traffic': '5K+ monthly',
|
||||
'implementation_time': '2-3 weeks',
|
||||
'roi_estimate': 'High ROI potential',
|
||||
'success_metrics': ['Traffic increase', 'Authority building', 'Lead generation']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def _get_fallback_market_position_analysis(self) -> Dict[str, Any]:
|
||||
"""Fallback market position analysis."""
|
||||
return {
|
||||
'market_leader': 'competitor1.com',
|
||||
'content_leader': 'competitor2.com',
|
||||
'quality_leader': 'competitor3.com',
|
||||
'market_gaps': ['Video content', 'Interactive content', 'Expert interviews'],
|
||||
'opportunities': ['Niche content development', 'Expert interviews', 'Industry reports'],
|
||||
'competitive_advantages': ['Technical expertise', 'Comprehensive guides', 'Industry insights']
|
||||
}
|
||||
|
||||
def _get_fallback_keyword_analysis(self) -> Dict[str, Any]:
|
||||
"""Fallback keyword analysis."""
|
||||
return {
|
||||
'keyword_opportunities': [
|
||||
{
|
||||
'keyword': 'industry best practices',
|
||||
'search_volume': 3000,
|
||||
'competition_level': 'low',
|
||||
'difficulty_score': 35,
|
||||
'trend': 'rising',
|
||||
'intent': 'informational',
|
||||
'opportunity_score': 85,
|
||||
'recommended_format': 'comprehensive_guide',
|
||||
'estimated_traffic': '2K+ monthly',
|
||||
'implementation_priority': 'high'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def _get_fallback_performance_prediction(self) -> Dict[str, Any]:
|
||||
"""Fallback performance prediction."""
|
||||
return {
|
||||
"traffic_predictions": {
|
||||
"estimated_monthly_traffic": "10K+",
|
||||
"traffic_growth_rate": "10%",
|
||||
"peak_traffic_month": "June",
|
||||
"confidence_level": "high"
|
||||
},
|
||||
"engagement_predictions": {
|
||||
"estimated_time_on_page": "5 min",
|
||||
"estimated_bounce_rate": "20%",
|
||||
"estimated_social_shares": "100+",
|
||||
"estimated_comments": "50+",
|
||||
"confidence_level": "medium"
|
||||
}
|
||||
}
|
||||
|
||||
def _get_fallback_strategic_intelligence(self) -> Dict[str, Any]:
|
||||
"""Fallback strategic intelligence."""
|
||||
return {
|
||||
"strategic_insights": [
|
||||
{
|
||||
"type": "content_strategy",
|
||||
"insight": "Focus on educational content to build authority",
|
||||
"reasoning": "Educational content is highly shareable and can attract a targeted audience.",
|
||||
"priority": "high",
|
||||
"estimated_impact": "Authority building",
|
||||
"implementation_time": "3-6 months",
|
||||
"confidence_level": "high"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def _get_fallback_content_quality_assessment(self) -> Dict[str, Any]:
|
||||
"""Fallback content quality assessment."""
|
||||
return {
|
||||
"overall_score": 88.0,
|
||||
"readability_score": 92.0,
|
||||
"seo_score": 95.0,
|
||||
"engagement_potential": "High engagement and retention",
|
||||
"improvement_suggestions": ["Add more internal links", "Optimize images for SEO"],
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
def get_performance_metrics(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get AI service performance metrics.
|
||||
|
||||
@@ -24,6 +24,8 @@ import asyncio
|
||||
import json
|
||||
import re
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
# Configure standard logging
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO, format='[%(asctime)s-%(levelname)s-%(module)s-%(lineno)d]- %(message)s')
|
||||
@@ -170,63 +172,107 @@ def gemini_pro_text_gen(prompt, temperature=0.7, top_p=0.9, top_k=40, max_tokens
|
||||
logger.error(f"Error in Gemini Pro text generation: {e}")
|
||||
return str(e)
|
||||
|
||||
def _dict_to_types_schema(schema: Dict[str, Any]) -> types.Schema:
|
||||
"""Convert a lightweight dict schema to google.genai.types.Schema."""
|
||||
if not isinstance(schema, dict):
|
||||
raise ValueError("response_schema must be a dict compatible with types.Schema")
|
||||
|
||||
def _convert(node: Dict[str, Any]) -> types.Schema:
|
||||
node_type = (node.get("type") or "OBJECT").upper()
|
||||
if node_type == "OBJECT":
|
||||
props = node.get("properties") or {}
|
||||
props_types: Dict[str, types.Schema] = {}
|
||||
for key, prop in props.items():
|
||||
if isinstance(prop, dict):
|
||||
props_types[key] = _convert(prop)
|
||||
else:
|
||||
props_types[key] = types.Schema(type=types.Type.STRING)
|
||||
return types.Schema(type=types.Type.OBJECT, properties=props_types if props_types else None)
|
||||
elif node_type == "ARRAY":
|
||||
items_node = node.get("items")
|
||||
if isinstance(items_node, dict):
|
||||
item_schema = _convert(items_node)
|
||||
else:
|
||||
item_schema = types.Schema(type=types.Type.STRING)
|
||||
return types.Schema(type=types.Type.ARRAY, items=item_schema)
|
||||
elif node_type == "NUMBER":
|
||||
return types.Schema(type=types.Type.NUMBER)
|
||||
elif node_type == "BOOLEAN":
|
||||
return types.Schema(type=types.Type.BOOLEAN)
|
||||
else:
|
||||
return types.Schema(type=types.Type.STRING)
|
||||
|
||||
return _convert(schema)
|
||||
|
||||
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
|
||||
def gemini_structured_json_response(prompt, schema, temperature=0.7, top_p=0.9, top_k=40, max_tokens=2048, system_prompt=None):
|
||||
"""
|
||||
Generate structured JSON response using Google's Gemini Pro model.
|
||||
|
||||
Args:
|
||||
prompt (str): The input text to generate completion for
|
||||
schema (dict): The JSON schema to follow for the response
|
||||
temperature (float, optional): Controls randomness. Defaults to 0.7
|
||||
top_p (float, optional): Controls diversity. Defaults to 0.9
|
||||
top_k (int, optional): Controls vocabulary size. Defaults to 40
|
||||
max_tokens (int, optional): Maximum number of tokens to generate. Defaults to 2048
|
||||
system_prompt (str, optional): System instructions for the model
|
||||
|
||||
Returns:
|
||||
dict: The generated structured JSON response
|
||||
"""
|
||||
try:
|
||||
# Configure the model
|
||||
client = genai.Client(api_key=os.getenv('GEMINI_API_KEY'))
|
||||
|
||||
# Set up generation config
|
||||
generation_config = {
|
||||
"temperature": temperature,
|
||||
"top_p": top_p,
|
||||
"top_k": top_k,
|
||||
"max_output_tokens": max_tokens,
|
||||
}
|
||||
|
||||
# Generate content with structured response
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-pro',
|
||||
contents=prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
system_instruction=system_prompt,
|
||||
max_output_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
top_k=top_k,
|
||||
response_mime_type='application/json',
|
||||
response_schema=schema
|
||||
),
|
||||
)
|
||||
|
||||
# Parse the response
|
||||
|
||||
# Build config using official SDK schema type
|
||||
try:
|
||||
# First try to get the parsed response
|
||||
if hasattr(response, 'parsed'):
|
||||
return response.parsed
|
||||
|
||||
# If parsed is not available, try to parse the text
|
||||
response_text = response.text
|
||||
return json.loads(response_text)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Error parsing JSON response: {e}")
|
||||
return {"error": f"Failed to parse JSON response: {e}", "raw_response": response_text}
|
||||
|
||||
types_schema = _dict_to_types_schema(schema) if isinstance(schema, dict) else schema
|
||||
except Exception as conv_err:
|
||||
logger.warning(f"Schema conversion warning, defaulting to OBJECT: {conv_err}")
|
||||
types_schema = types.Schema(type=types.Type.OBJECT)
|
||||
|
||||
generation_config = types.GenerateContentConfig(
|
||||
system_instruction=system_prompt,
|
||||
max_output_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
top_k=top_k,
|
||||
response_mime_type='application/json',
|
||||
response_schema=types_schema
|
||||
)
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=prompt,
|
||||
config=generation_config,
|
||||
)
|
||||
|
||||
# Prefer parsed if present and non-empty; otherwise parse text with fallbacks
|
||||
try:
|
||||
parsed = getattr(response, 'parsed', None)
|
||||
if parsed:
|
||||
return parsed if isinstance(parsed, dict) else json.loads(json.dumps(parsed))
|
||||
text = (response.text or '').strip()
|
||||
# Strip markdown code fences if present
|
||||
if text.startswith('```'):
|
||||
# remove leading ```json or ``` and trailing ```
|
||||
if text.lower().startswith('```json'):
|
||||
text = text[7:]
|
||||
else:
|
||||
text = text[3:]
|
||||
if text.endswith('```'):
|
||||
text = text[:-3]
|
||||
text = text.strip()
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
# Fallback: extract likely JSON object substring
|
||||
first = text.find('{')
|
||||
last = text.rfind('}')
|
||||
if first != -1 and last != -1 and last > first:
|
||||
candidate = text[first:last+1]
|
||||
try:
|
||||
return json.loads(candidate)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
# Final fallback: regex any object
|
||||
import re
|
||||
match = re.search(r'\{[\s\S]*\}', text)
|
||||
if match:
|
||||
return json.loads(match.group(0))
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing structured response: {e}")
|
||||
return {"error": f"Failed to parse JSON response: {e}", "raw_response": (response.text or '')}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in Gemini Pro structured JSON generation: {e}")
|
||||
return {"error": str(e)}
|
||||
@@ -79,8 +79,8 @@ def llm_text_gen(prompt: str, system_prompt: Optional[str] = None, json_struct:
|
||||
elif gpt_provider == "deepseek":
|
||||
model = "deepseek-chat"
|
||||
else:
|
||||
logger.warning("[llm_text_gen] No API keys found, using mock response")
|
||||
return _get_mock_response(prompt)
|
||||
logger.error("[llm_text_gen] No API keys found. Structured mock responses are disabled.")
|
||||
raise RuntimeError("No LLM API keys configured. Configure provider API keys to enable AI responses.")
|
||||
|
||||
logger.debug(f"[llm_text_gen] Using provider: {gpt_provider}, model: {model}")
|
||||
|
||||
@@ -163,7 +163,7 @@ def llm_text_gen(prompt: str, system_prompt: Optional[str] = None, json_struct:
|
||||
)
|
||||
else:
|
||||
logger.error(f"[llm_text_gen] Unknown provider: {gpt_provider}")
|
||||
return _get_mock_response(prompt)
|
||||
raise RuntimeError("Unknown LLM provider.")
|
||||
except Exception as provider_error:
|
||||
logger.error(f"[llm_text_gen] Provider {gpt_provider} failed: {str(provider_error)}")
|
||||
# Try to fallback to another provider
|
||||
@@ -203,85 +203,13 @@ def llm_text_gen(prompt: str, system_prompt: Optional[str] = None, json_struct:
|
||||
logger.error(f"[llm_text_gen] Fallback provider {fallback_provider} also failed: {str(fallback_error)}")
|
||||
continue
|
||||
|
||||
# If all providers fail, return mock response
|
||||
logger.warning("[llm_text_gen] All providers failed, using mock response")
|
||||
return _get_mock_response(prompt)
|
||||
# If all providers fail, raise an error (no mock)
|
||||
logger.error("[llm_text_gen] All providers failed. Structured mock responses are disabled.")
|
||||
raise RuntimeError("All LLM providers failed to generate a response.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[llm_text_gen] Error during text generation: {str(e)}")
|
||||
return _get_mock_response(prompt)
|
||||
|
||||
def _get_mock_response(prompt: str) -> str:
|
||||
"""Get a mock response when no API keys are available."""
|
||||
logger.warning("[llm_text_gen] Using mock response - no API keys configured")
|
||||
|
||||
# Return a structured mock response for style detection
|
||||
if "style analysis" in prompt.lower() or "writing style" in prompt.lower():
|
||||
return json.dumps({
|
||||
"writing_style": {
|
||||
"tone": "professional",
|
||||
"voice": "active",
|
||||
"complexity": "moderate",
|
||||
"engagement_level": "high"
|
||||
},
|
||||
"content_characteristics": {
|
||||
"sentence_structure": "well-structured",
|
||||
"vocabulary_level": "intermediate",
|
||||
"paragraph_organization": "logical flow",
|
||||
"content_flow": "smooth transitions"
|
||||
},
|
||||
"target_audience": {
|
||||
"demographics": ["professionals", "business users"],
|
||||
"expertise_level": "intermediate",
|
||||
"industry_focus": "technology",
|
||||
"geographic_focus": "global"
|
||||
},
|
||||
"content_type": {
|
||||
"primary_type": "blog",
|
||||
"secondary_types": ["article", "guide"],
|
||||
"purpose": "inform",
|
||||
"call_to_action": "moderate"
|
||||
},
|
||||
"recommended_settings": {
|
||||
"writing_tone": "professional",
|
||||
"target_audience": "business professionals",
|
||||
"content_type": "blog",
|
||||
"creativity_level": "medium",
|
||||
"geographic_location": "global"
|
||||
}
|
||||
})
|
||||
|
||||
# Handle pattern analysis requests
|
||||
if "pattern" in prompt.lower() or "recurring" in prompt.lower():
|
||||
return json.dumps({
|
||||
"patterns": {
|
||||
"sentence_length": "medium",
|
||||
"vocabulary_patterns": ["technical terms", "professional language"],
|
||||
"rhetorical_devices": ["examples", "analogies"],
|
||||
"paragraph_structure": "topic sentence followed by supporting details",
|
||||
"transition_phrases": ["furthermore", "additionally", "however"]
|
||||
},
|
||||
"style_consistency": "high",
|
||||
"unique_elements": ["clear structure", "professional tone", "evidence-based content"]
|
||||
})
|
||||
|
||||
# Handle guidelines generation requests
|
||||
if "guidelines" in prompt.lower() or "recommendations" in prompt.lower():
|
||||
return json.dumps({
|
||||
"guidelines": {
|
||||
"tone_recommendations": ["maintain professional tone", "use clear language"],
|
||||
"structure_guidelines": ["start with introduction", "use headings", "conclude with summary"],
|
||||
"vocabulary_suggestions": ["avoid jargon", "use industry-specific terms appropriately"],
|
||||
"engagement_tips": ["include examples", "use active voice", "ask questions"],
|
||||
"audience_considerations": ["consider technical level", "provide context"]
|
||||
},
|
||||
"best_practices": ["research thoroughly", "cite sources", "update regularly"],
|
||||
"avoid_elements": ["overly technical language", "long paragraphs", "passive voice"],
|
||||
"content_strategy": "focus on providing value while maintaining professional credibility"
|
||||
})
|
||||
|
||||
# Generic mock response for other content generation
|
||||
return "This is a mock response. Please configure API keys for real content generation. To get started, visit the onboarding process and configure your AI provider API keys."
|
||||
raise
|
||||
|
||||
def check_gpt_provider(gpt_provider: str) -> bool:
|
||||
"""Check if the specified GPT provider is supported."""
|
||||
|
||||
103
docs/autofill_learning_personalization.md
Normal file
103
docs/autofill_learning_personalization.md
Normal file
@@ -0,0 +1,103 @@
|
||||
### Autofill: Learning, Personalization, and Explainability
|
||||
|
||||
This document outlines next-step enhancements for Content Strategy Autofill focusing on: learning from user acceptances, industry presets, constraint-aware generation, explainability, and RAG-lite context. It also captures the trade-offs for sectioned generation vs single-call generation.
|
||||
|
||||
## Goals
|
||||
- Increase accuracy, personalization, and trust without increasing UI complexity.
|
||||
- Keep costs predictable while reducing timeouts and retries.
|
||||
- Preserve user control: never overwrite locked/accepted fields without consent.
|
||||
|
||||
## Single-call vs Sectioned Generation
|
||||
- Single-call (current):
|
||||
- Pros: 1 AI request, simpler orchestration.
|
||||
- Cons: Larger prompt, higher timeout risk, brittle for structured JSON, hard to pinpoint failures.
|
||||
- Sectioned (per category):
|
||||
- Pros: Shorter prompts, better accuracy, quicker partial results, granular retries; lower latency per section; easier streaming (“Category X complete”).
|
||||
- Cons: More calls; must cap/parallelize and cache to control cost.
|
||||
- Recommendation: Hybrid
|
||||
- Default: single-call for fast baseline; fallback/option: sectioned generation for users with large sites or when single-call fails/times out.
|
||||
- Implement a server flag `mode=hybrid|single|sectioned` and a per-user policy (feature flag).
|
||||
|
||||
## Learning from Acceptances
|
||||
- Data we already persist: `content_strategy_autofill_insights` (accepted fields + sources/meta).
|
||||
- Learning policy:
|
||||
- Build a per-user profile vector of “accepted values” and “field tendencies” (e.g., formats: video, cadence: weekly; brand voice: authoritative).
|
||||
- During refresh:
|
||||
- Use these as soft priors in prompt (“Bias toward previously accepted values unless contradictory to new constraints”).
|
||||
- Prefer stable fields to remain unchanged unless explicitly unconstrained.
|
||||
- Storage additions:
|
||||
- Add fields to `content_strategy_autofill_insights` meta: `industry`, `company_size`, `accepted_at`.
|
||||
- Maintain a compact, cached user profile (derived) for prompt injection.
|
||||
- Safety:
|
||||
- Respect locked fields (frontend lock) → never modified by refresh.
|
||||
|
||||
## Industry Presets
|
||||
- Purpose: Cold-start quality boost.
|
||||
- Source: curated presets per industry, company size, and region.
|
||||
- Shape:
|
||||
- Minimal key set aligned to core inputs (e.g., `preferred_formats`, `content_frequency`, `brand_voice`, `editorial_guidelines` template).
|
||||
- Retrieval:
|
||||
- Endpoint: GET `/autofill/presets?industry=...&size=...®ion=...` (cached).
|
||||
- Merge policy:
|
||||
- Apply only to empty fields; AI may override if constraints request.
|
||||
|
||||
## Constraint-Aware Generation
|
||||
- User constraints: budget ceiling, cadence/frequency, format allowlist, timeline bounds.
|
||||
- UI:
|
||||
- “Constraints” panel (chip-set) accessible from header/Progress area.
|
||||
- Backend:
|
||||
- Accept constraints in refresh request (query/body).
|
||||
- Inject constraints into prompt header and soft-validate outputs.
|
||||
- Validation:
|
||||
- Enforce with server-side validators; warn if AI violates, and auto-correct when safe.
|
||||
|
||||
## Explain This Suggestion (Mini-modal)
|
||||
- Trigger: info icon next to each field.
|
||||
- Content:
|
||||
- Short justification text (one or two sentences), sources (onboarding/RAG docs), confidence.
|
||||
- No raw chain-of-thought; ask model for a concise rationale summary that’s safe to expose.
|
||||
- Backend payload additions:
|
||||
- For each field: `meta[field] = { rationale: string, sources: string[] }` (optional).
|
||||
- Caution: redact sensitive content; keep rationale brief and non-speculative.
|
||||
|
||||
## RAG-lite: Retrievable Context for Refresh
|
||||
- Context sources:
|
||||
- Latest website crawl snippets (top pages, headings, meta), recent analytics top pages (if connected), competitor headlines if available.
|
||||
- Ingestion:
|
||||
- Lightweight index (in-memory/SQLite) with page URL, title, summary; refresh on demand with TTL.
|
||||
- Prompt strategy:
|
||||
- Provide 3–5 top relevant snippets per category; keep token budget small.
|
||||
- Controls:
|
||||
- User toggle “Use live site signals” in refresh.
|
||||
|
||||
## API Additions
|
||||
- Refresh
|
||||
- GET `/autofill/refresh/stream?ai_only=true&constraints=...&mode=hybrid&use_rag=true`
|
||||
- Non-stream POST variant mirrors params.
|
||||
- Presets
|
||||
- GET `/autofill/presets?industry=...&size=...®ion=...` → returns compact preset payload.
|
||||
- Acceptances (existing)
|
||||
- POST `/{strategy_id}/autofill/accept` → persist accepted fields with transparency/meta.
|
||||
|
||||
## UI Enhancements
|
||||
- Per-field lock and regenerate
|
||||
- Lock prevents overwrite; Regenerate calls sectioned refresh for that field’s category.
|
||||
- Diff view on refresh
|
||||
- Show before → after per field with accept/revert quick actions.
|
||||
- Constraints chips
|
||||
- Visible summary in header; edit inline.
|
||||
- “Explain” modal
|
||||
- Shows rationale and sources for the current value.
|
||||
|
||||
## Observability & Metrics
|
||||
- Track per-field fill-rate, violation corrections, latency (per section), AI cost per refresh.
|
||||
- Alert on sudden drops in non-null field count or spike in violations/timeouts.
|
||||
|
||||
## Rollout Plan
|
||||
1) Phase 1 (Low risk): presets + constraints + per-field lock, no sectioning.
|
||||
2) Phase 2: sectioned generation behind a feature flag; per-field regenerate.
|
||||
3) Phase 3: RAG-lite snippets and explain modal; start learning from acceptances in prompts.
|
||||
4) Phase 4: tune/fine-grain priors and add advanced validation rules per industry.
|
||||
|
||||
## References
|
||||
- Gemini structured output: https://ai.google.dev/gemini-api/docs/structured-output
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.c9966057.css",
|
||||
"main.js": "/static/js/main.ba50e996.js",
|
||||
"main.js": "/static/js/main.c6e229ae.js",
|
||||
"index.html": "/index.html",
|
||||
"main.c9966057.css.map": "/static/css/main.c9966057.css.map",
|
||||
"main.ba50e996.js.map": "/static/js/main.ba50e996.js.map"
|
||||
"main.c6e229ae.js.map": "/static/js/main.c6e229ae.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.c9966057.css",
|
||||
"static/js/main.ba50e996.js"
|
||||
"static/js/main.c6e229ae.js"
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Alwrity - AI Content Creation Platform"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Alwrity - AI Content Creation Platform</title><script defer="defer" src="/static/js/main.ba50e996.js"></script><link href="/static/css/main.c9966057.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Alwrity - AI Content Creation Platform"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Alwrity - AI Content Creation Platform</title><script defer="defer" src="/static/js/main.c6e229ae.js"></script><link href="/static/css/main.c9966057.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
@@ -71,6 +71,7 @@ import { getEducationalContent } from './ContentStrategyBuilder/utils/educationa
|
||||
import CategoryList from './ContentStrategyBuilder/components/CategoryList';
|
||||
import ProgressTracker from './ContentStrategyBuilder/components/ProgressTracker';
|
||||
import HeaderSection from './ContentStrategyBuilder/components/HeaderSection';
|
||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||
|
||||
const ContentStrategyBuilder: React.FC = () => {
|
||||
const {
|
||||
@@ -112,6 +113,10 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
const [showEducationalInfo, setShowEducationalInfo] = useState<string | null>(null);
|
||||
const [showAIRecommendations, setShowAIRecommendations] = useState(false);
|
||||
const [showDataSourceTransparency, setShowDataSourceTransparency] = useState(false);
|
||||
const [refreshMessage, setRefreshMessage] = useState<string | null>(null);
|
||||
const [refreshProgress, setRefreshProgress] = useState<number>(0);
|
||||
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
|
||||
const [refreshError, setRefreshError] = useState<string | null>(null);
|
||||
|
||||
// Ref to track if we've already set the default category
|
||||
const hasSetDefaultCategory = useRef(false);
|
||||
@@ -310,8 +315,20 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
|
||||
{/* Error Alert */}
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
{error}
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{ mb: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
|
||||
action={
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button size="small" variant="outlined" onClick={() => autoPopulateFromOnboarding(true)} startIcon={<RefreshIcon />}>Retry</Button>
|
||||
<Button size="small" variant="contained" color="primary" onClick={() => setShowDataSourceTransparency(true)} startIcon={<InfoIcon />}>Why?</Button>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="subtitle2">Real data required</Typography>
|
||||
<Typography variant="body2">{error || 'We could not auto-populate because required onboarding/analysis data is missing. Connect sources or complete onboarding, then retry.'}</Typography>
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -380,7 +397,87 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
aiGenerating={aiGenerating}
|
||||
onShowAIRecommendations={() => setShowAIRecommendations(true)}
|
||||
onShowDataSourceTransparency={() => setShowDataSourceTransparency(true)}
|
||||
onRefreshData={autoPopulateFromOnboarding}
|
||||
onRefreshData={() => autoPopulateFromOnboarding()}
|
||||
onRefreshAI={async () => {
|
||||
try {
|
||||
setAIGenerating(true);
|
||||
setIsRefreshing(true);
|
||||
setRefreshError(null);
|
||||
setRefreshMessage('Initializing refresh…');
|
||||
setRefreshProgress(5);
|
||||
const es = await contentPlanningApi.streamAutofillRefresh(1, true, true);
|
||||
es.onmessage = (evt: MessageEvent) => {
|
||||
try {
|
||||
const data = JSON.parse(evt.data);
|
||||
if (data.type === 'status' || data.type === 'progress') {
|
||||
setRefreshMessage(data.message || 'Refreshing…');
|
||||
if (typeof data.progress === 'number') setRefreshProgress(data.progress);
|
||||
}
|
||||
if (data.type === 'result') {
|
||||
const payload = data.data || {};
|
||||
const fields = payload.fields || {};
|
||||
const sources = payload.sources || {};
|
||||
const inputDataPoints = payload.input_data_points || {};
|
||||
const meta = payload.meta || {};
|
||||
const fieldValues: Record<string, any> = {};
|
||||
Object.keys(fields).forEach((fieldId) => {
|
||||
const fieldData = fields[fieldId];
|
||||
if (fieldData && typeof fieldData === 'object' && 'value' in fieldData) {
|
||||
fieldValues[fieldId] = fieldData.value;
|
||||
}
|
||||
});
|
||||
useEnhancedStrategyStore.setState((state) => ({
|
||||
autoPopulatedFields: { ...state.autoPopulatedFields, ...fieldValues },
|
||||
dataSources: { ...state.dataSources, ...sources },
|
||||
inputDataPoints,
|
||||
formData: { ...state.formData, ...fieldValues }
|
||||
}));
|
||||
if (!meta.ai_used || meta.ai_overrides_count === 0) {
|
||||
const msg = 'AI did not produce new values. Please try again or complete onboarding data.';
|
||||
setError(msg);
|
||||
setRefreshError(msg);
|
||||
setRefreshMessage('No new AI values available.');
|
||||
}
|
||||
es.close();
|
||||
setAIGenerating(false);
|
||||
setIsRefreshing(false);
|
||||
if (!meta || meta.ai_overrides_count > 0) {
|
||||
setRefreshMessage(null);
|
||||
setRefreshProgress(0);
|
||||
}
|
||||
}
|
||||
if (data.type === 'error') {
|
||||
const msg = data.message || 'AI refresh failed.';
|
||||
setRefreshError(msg);
|
||||
es.close();
|
||||
setAIGenerating(false);
|
||||
setIsRefreshing(false);
|
||||
setRefreshMessage('Refresh failed.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('SSE parse error:', err);
|
||||
}
|
||||
};
|
||||
es.onerror = (err: any) => {
|
||||
console.error('SSE connection error:', err);
|
||||
es.close();
|
||||
setAIGenerating(false);
|
||||
setIsRefreshing(false);
|
||||
setRefreshError('AI refresh connection lost. Please try again.');
|
||||
setRefreshMessage('Connection lost.');
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('AI refresh error', e);
|
||||
setAIGenerating(false);
|
||||
setIsRefreshing(false);
|
||||
setRefreshError('AI refresh failed. Please try again.');
|
||||
setRefreshMessage('Refresh failed.');
|
||||
}
|
||||
}}
|
||||
refreshMessage={refreshMessage}
|
||||
refreshProgress={refreshProgress}
|
||||
isRefreshing={isRefreshing}
|
||||
refreshError={refreshError}
|
||||
/>
|
||||
|
||||
{/* Category Progress - Compact with Futuristic Styling */}
|
||||
@@ -428,7 +525,7 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={autoPopulateFromOnboarding}
|
||||
onClick={() => autoPopulateFromOnboarding(true)}
|
||||
fullWidth
|
||||
>
|
||||
Refresh Data
|
||||
@@ -518,8 +615,8 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
{/* Category Fields */}
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Grid container spacing={2}>
|
||||
{STRATEGIC_INPUT_FIELDS
|
||||
.filter(field => field.category === activeCategory)
|
||||
{STRATEGIC_INPUT_FIELDS
|
||||
.filter(field => field.category === activeCategory)
|
||||
.map((field, index) => {
|
||||
// Determine grid size based on field type for better layout organization
|
||||
const type = field.type;
|
||||
@@ -531,30 +628,30 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
const gridMd = forceFullWidth ? 12 : (isWideField ? 12 : isMediumField ? 6 : 4);
|
||||
const gridLg = forceFullWidth ? 12 : (isWideField ? 12 : isMediumField ? 6 : 4);
|
||||
const gridSm = 12;
|
||||
|
||||
return (
|
||||
|
||||
return (
|
||||
<Grid item xs={12} sm={gridSm} md={gridMd} lg={gridLg} key={field.id}>
|
||||
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.25, delay: index * 0.03 }}>
|
||||
<StrategicInputField
|
||||
fieldId={field.id}
|
||||
value={formData[field.id]}
|
||||
error={formErrors[field.id]}
|
||||
autoPopulated={!!autoPopulatedFields[field.id]}
|
||||
dataSource={dataSources[field.id]}
|
||||
confidenceLevel={autoPopulatedFields[field.id] ? 0.8 : undefined}
|
||||
dataQuality={autoPopulatedFields[field.id] ? 'High Quality' : undefined}
|
||||
onChange={(value: any) => updateFormField(field.id, value)}
|
||||
onValidate={() => validateFormField(field.id)}
|
||||
onShowTooltip={() => setShowTooltip(field.id)}
|
||||
<StrategicInputField
|
||||
fieldId={field.id}
|
||||
value={formData[field.id]}
|
||||
error={formErrors[field.id]}
|
||||
autoPopulated={!!autoPopulatedFields[field.id]}
|
||||
dataSource={dataSources[field.id]}
|
||||
confidenceLevel={autoPopulatedFields[field.id] ? 0.8 : undefined}
|
||||
dataQuality={autoPopulatedFields[field.id] ? 'High Quality' : undefined}
|
||||
onChange={(value: any) => updateFormField(field.id, value)}
|
||||
onValidate={() => validateFormField(field.id)}
|
||||
onShowTooltip={() => setShowTooltip(field.id)}
|
||||
onViewDataSource={() => setShowDataSourceTransparency(true)}
|
||||
accentColorKey={getCategoryColor(activeCategory) as any}
|
||||
isCompact={isCompactField}
|
||||
/>
|
||||
/>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* Category Actions */}
|
||||
@@ -567,26 +664,26 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
reviewedCategories: Array.from(reviewedCategories)
|
||||
});
|
||||
return !isReviewed ? (
|
||||
<Button
|
||||
variant="contained"
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
console.log('🔘 Button clicked! activeCategory:', activeCategory);
|
||||
console.log('🔘 reviewedCategories:', Array.from(reviewedCategories));
|
||||
console.log('🔘 isMarkingReviewed:', isMarkingReviewed);
|
||||
handleConfirmCategoryReviewWrapper();
|
||||
}}
|
||||
startIcon={isMarkingReviewed ? <CircularProgress size={20} /> : <CheckCircleIcon />}
|
||||
disabled={isMarkingReviewed}
|
||||
>
|
||||
{isMarkingReviewed ? 'Marking as Reviewed...' : 'Mark as Reviewed'}
|
||||
</Button>
|
||||
) : (
|
||||
<Chip
|
||||
label="Category Reviewed"
|
||||
color="success"
|
||||
icon={<CheckCircleIcon />}
|
||||
sx={{ px: 2, py: 1 }}
|
||||
/>
|
||||
startIcon={isMarkingReviewed ? <CircularProgress size={20} /> : <CheckCircleIcon />}
|
||||
disabled={isMarkingReviewed}
|
||||
>
|
||||
{isMarkingReviewed ? 'Marking as Reviewed...' : 'Mark as Reviewed'}
|
||||
</Button>
|
||||
) : (
|
||||
<Chip
|
||||
label="Category Reviewed"
|
||||
color="success"
|
||||
icon={<CheckCircleIcon />}
|
||||
sx={{ px: 2, py: 1 }}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
|
||||
|
||||
@@ -24,6 +24,12 @@ interface ProgressTrackerProps {
|
||||
onShowAIRecommendations: () => void;
|
||||
onShowDataSourceTransparency: () => void;
|
||||
onRefreshData: () => void;
|
||||
onRefreshAI?: () => void;
|
||||
// New optional props for refresh feedback
|
||||
refreshMessage?: string | null;
|
||||
refreshProgress?: number;
|
||||
isRefreshing?: boolean;
|
||||
refreshError?: string | null;
|
||||
}
|
||||
|
||||
const ProgressTracker: React.FC<ProgressTrackerProps> = ({
|
||||
@@ -34,28 +40,25 @@ const ProgressTracker: React.FC<ProgressTrackerProps> = ({
|
||||
aiGenerating,
|
||||
onShowAIRecommendations,
|
||||
onShowDataSourceTransparency,
|
||||
onRefreshData
|
||||
onRefreshData,
|
||||
onRefreshAI,
|
||||
refreshMessage,
|
||||
refreshProgress = 0,
|
||||
isRefreshing = false,
|
||||
refreshError = null
|
||||
}) => {
|
||||
const effectiveProgress = isRefreshing ? Math.max(5, Math.min(100, Math.round(refreshProgress))) : Math.round(reviewProgressPercentage);
|
||||
|
||||
return (
|
||||
<Box sx={{ mb: 1.5 }}>
|
||||
{/* Compact header row with title, progress, counts and actions */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.75 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="h6" sx={{ mb: 0, fontSize: '1rem' }}>
|
||||
Progress
|
||||
</Typography>
|
||||
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
|
||||
<CircularProgress
|
||||
variant="determinate"
|
||||
value={reviewProgressPercentage}
|
||||
size={28}
|
||||
thickness={4}
|
||||
sx={{ color: 'primary.main', '& .MuiCircularProgress-circle': { strokeLinecap: 'round' } }}
|
||||
/>
|
||||
<Box sx={{ top: 0, left: 0, bottom: 0, right: 0, position: 'absolute', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Typography variant="caption" component="div" color="text.secondary" sx={{ fontSize: '0.65rem', fontWeight: 700 }}>
|
||||
{`${Math.round(reviewProgressPercentage)}%`}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>Progress</Typography>
|
||||
<Box sx={{ position: 'relative', width: 28, height: 28 }}>
|
||||
<CircularProgress variant="determinate" value={effectiveProgress} size={28} thickness={5} />
|
||||
<Box sx={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Typography variant="caption" sx={{ fontSize: 10 }}>{effectiveProgress}%</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
|
||||
@@ -64,49 +67,44 @@ const ProgressTracker: React.FC<ProgressTrackerProps> = ({
|
||||
</Box>
|
||||
|
||||
{/* Actions inline in header */}
|
||||
<Box sx={{ display: 'flex', gap: 0.75 }}>
|
||||
<MuiTooltip title="View AI-powered recommendations and insights" placement="top">
|
||||
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||
<IconButton
|
||||
onClick={onShowAIRecommendations}
|
||||
sx={{ color: 'primary.main', bgcolor: 'rgba(255, 193, 7, 0.08)', border: '1px solid rgba(255, 193, 7, 0.25)', width: 32, height: 32, '&:hover': { bgcolor: 'rgba(255, 193, 7, 0.16)' } }}
|
||||
>
|
||||
<Badge badgeContent={5} sx={{ '& .MuiBadge-badge': { fontSize: '0.55rem', fontWeight: 700, bgcolor: '#ff6b35', color: 'white' } }}>
|
||||
<AutoAwesomeIcon sx={{ fontSize: 16 }} />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</motion.div>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<MuiTooltip title="AI Recommendations">
|
||||
<IconButton size="small" onClick={onShowAIRecommendations}>
|
||||
<AutoAwesomeIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MuiTooltip>
|
||||
|
||||
<MuiTooltip title="View data sources and transparency information" placement="top">
|
||||
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||
<IconButton
|
||||
onClick={onShowDataSourceTransparency}
|
||||
sx={{ color: 'primary.main', bgcolor: 'rgba(76, 175, 80, 0.08)', border: '1px solid rgba(76, 175, 80, 0.25)', width: 32, height: 32, '&:hover': { bgcolor: 'rgba(76, 175, 80, 0.16)' } }}
|
||||
>
|
||||
<Badge badgeContent={Object.keys(autoPopulatedFields || {}).length} sx={{ '& .MuiBadge-badge': { fontSize: '0.55rem', fontWeight: 700, bgcolor: '#2196f3', color: 'white' } }}>
|
||||
<InfoIcon sx={{ fontSize: 16 }} />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</motion.div>
|
||||
<MuiTooltip title="Data Transparency">
|
||||
<IconButton size="small" onClick={onShowDataSourceTransparency}>
|
||||
<InfoIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MuiTooltip>
|
||||
|
||||
<MuiTooltip title="Refresh auto-populated data" placement="top">
|
||||
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||
<IconButton onClick={onRefreshData} sx={{ color: 'primary.main', bgcolor: 'rgba(0,0,0,0.04)', border: '1px solid rgba(0,0,0,0.12)', width: 32, height: 32, '&:hover': { bgcolor: 'rgba(0,0,0,0.08)' } }}>
|
||||
<RefreshIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</motion.div>
|
||||
<MuiTooltip title="Refresh Data (AI)">
|
||||
<IconButton size="small" onClick={onRefreshAI || onRefreshData}>
|
||||
<RefreshIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MuiTooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Combined info line */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{/* Combined info line with refresh/error banner */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minHeight: 22 }}>
|
||||
<CheckCircleIcon color="success" sx={{ fontSize: 14 }} />
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
|
||||
Auto-population: {Object.keys(autoPopulatedFields || {}).length} fields • AI Insights: {aiGenerating ? 'Generating...' : 'Ready'}
|
||||
</Typography>
|
||||
{refreshError ? (
|
||||
<Typography variant="caption" color="error" sx={{ fontSize: '0.72rem' }}>
|
||||
{refreshError}
|
||||
</Typography>
|
||||
) : isRefreshing ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<CircularProgress size={12} thickness={6} />
|
||||
<Typography variant="caption" color="primary" sx={{ fontSize: '0.72rem' }}>
|
||||
{refreshMessage || 'Refreshing data…'}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
|
||||
Auto-population: {Object.keys(autoPopulatedFields || {}).length} fields • AI Insights: {aiGenerating ? 'Generating…' : 'Ready'}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -658,6 +658,36 @@ class ContentPlanningAPI {
|
||||
return new EventSource(url);
|
||||
}
|
||||
|
||||
// Clear enhanced strategy streaming/cache for a user (best-effort refresh)
|
||||
async clearEnhancedCache(userId?: number): Promise<any> {
|
||||
const params: any = {};
|
||||
if (userId) params.user_id = userId;
|
||||
const response = await apiClient.post(`${this.baseURL}/enhanced-strategies/cache/clear`, null, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Stream AI generation/status updates for a specific strategy (best-effort)
|
||||
async streamAIGenerationStatus(strategyId: number | string): Promise<EventSource> {
|
||||
const url = `${this.baseURL}/enhanced-strategies/stream/strategies?strategy_id=${strategyId}`;
|
||||
return new EventSource(url);
|
||||
}
|
||||
|
||||
async streamAutofillRefresh(userId?: number, useAI: boolean = true, aiOnly: boolean = false): Promise<EventSource> {
|
||||
const params = new URLSearchParams();
|
||||
if (userId) params.append('user_id', String(userId));
|
||||
params.append('use_ai', String(useAI));
|
||||
params.append('ai_only', String(aiOnly));
|
||||
const url = `${this.baseURL}/enhanced-strategies/autofill/refresh/stream?${params.toString()}`;
|
||||
return new EventSource(url);
|
||||
}
|
||||
|
||||
async refreshAutofill(userId?: number, useAI: boolean = true, aiOnly: boolean = false): Promise<any> {
|
||||
const params: any = { use_ai: useAI, ai_only: aiOnly };
|
||||
if (userId) params.user_id = userId;
|
||||
const response = await apiClient.post(`${this.baseURL}/enhanced-strategies/autofill/refresh`, null, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Helper method to handle SSE data
|
||||
handleSSEData(eventSource: EventSource, onData: (data: any) => void, onError?: (error: any) => void, onComplete?: () => void) {
|
||||
eventSource.onmessage = (event) => {
|
||||
|
||||
@@ -26,6 +26,7 @@ export class ContentPlanningOrchestrator {
|
||||
private serviceStatuses: Map<string, ServiceStatus> = new Map();
|
||||
private onProgressUpdate?: (statuses: ServiceStatus[]) => void;
|
||||
private onDataUpdate?: (data: Partial<DashboardData>) => void;
|
||||
private latestDashboardData: DashboardData | null = null;
|
||||
|
||||
constructor() {
|
||||
this.initializeServiceStatuses();
|
||||
@@ -128,6 +129,7 @@ export class ContentPlanningOrchestrator {
|
||||
}
|
||||
});
|
||||
|
||||
this.latestDashboardData = dashboardData;
|
||||
return dashboardData;
|
||||
}
|
||||
|
||||
@@ -227,51 +229,77 @@ export class ContentPlanningOrchestrator {
|
||||
message: 'Initializing AI analysis...'
|
||||
});
|
||||
|
||||
return new Promise<{ aiInsights: any[]; aiRecommendations: any[] }>((resolve, reject) => {
|
||||
contentPlanningApi.streamAIAnalytics(
|
||||
// Progress callback
|
||||
(progressData) => {
|
||||
this.updateServiceStatus('aiAnalytics', {
|
||||
progress: progressData.progress,
|
||||
message: progressData.message || 'AI analysis in progress...'
|
||||
});
|
||||
},
|
||||
// Complete callback
|
||||
(aiData) => {
|
||||
this.updateServiceStatus('aiAnalytics', {
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
message: `Generated ${aiData.insights?.length || 0} insights and ${aiData.recommendations?.length || 0} recommendations`,
|
||||
data: aiData
|
||||
});
|
||||
|
||||
this.notifyDataUpdate({
|
||||
aiInsights: aiData.insights || [],
|
||||
aiRecommendations: aiData.recommendations || []
|
||||
});
|
||||
|
||||
resolve({
|
||||
aiInsights: aiData.insights || [],
|
||||
aiRecommendations: aiData.recommendations || []
|
||||
});
|
||||
},
|
||||
// Error callback
|
||||
(error) => {
|
||||
this.updateServiceStatus('aiAnalytics', {
|
||||
status: 'error',
|
||||
progress: 0,
|
||||
message: 'AI analysis failed',
|
||||
error: error.message
|
||||
});
|
||||
reject(error);
|
||||
// New approach: stream strategic intelligence data and show status from AI generation SSE
|
||||
return await new Promise<{ aiInsights: any[]; aiRecommendations: any[] }>(async (resolve) => {
|
||||
// 1) Execution status stream (best-effort; ignore if no active strategy)
|
||||
try {
|
||||
const currentStrategyId = this.latestDashboardData?.strategies?.[0]?.id;
|
||||
if (currentStrategyId) {
|
||||
const statusSource = await contentPlanningApi.streamAIGenerationStatus(currentStrategyId);
|
||||
statusSource.onmessage = (event: MessageEvent) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'progress') {
|
||||
this.updateServiceStatus('aiAnalytics', {
|
||||
status: 'loading',
|
||||
progress: Math.min(99, data.progress || 20),
|
||||
message: data.detail || 'AI generation in progress...'
|
||||
});
|
||||
}
|
||||
if (data.type === 'result') {
|
||||
this.updateServiceStatus('aiAnalytics', {
|
||||
status: data.status === 'completed' ? 'success' : 'error',
|
||||
progress: 100,
|
||||
message: data.status === 'completed' ? 'AI generation completed' : 'AI generation failed'
|
||||
});
|
||||
statusSource.close();
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
statusSource.onerror = () => statusSource.close();
|
||||
}
|
||||
);
|
||||
} catch {}
|
||||
|
||||
// 2) Data stream for insights (Strategic Intelligence)
|
||||
const intelSource = await contentPlanningApi.streamStrategicIntelligence(1);
|
||||
contentPlanningApi.handleSSEData(
|
||||
intelSource,
|
||||
(data) => {
|
||||
if (data.type === 'progress') {
|
||||
this.updateServiceStatus('aiAnalytics', {
|
||||
status: 'loading',
|
||||
progress: Math.max(20, data.progress || 40),
|
||||
message: data.message || 'Analyzing strategic intelligence...'
|
||||
});
|
||||
} else if (data.type === 'result' && data.status === 'success') {
|
||||
this.updateServiceStatus('aiAnalytics', {
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
message: 'Strategic intelligence ready',
|
||||
data: data.data
|
||||
});
|
||||
// Map to orchestrator fields if needed
|
||||
this.notifyDataUpdate({ aiInsights: data.data?.recommendations || [], aiRecommendations: [] });
|
||||
resolve({ aiInsights: data.data?.recommendations || [], aiRecommendations: [] });
|
||||
} else if (data.type === 'error') {
|
||||
this.updateServiceStatus('aiAnalytics', {
|
||||
status: 'error',
|
||||
progress: 0,
|
||||
message: data.message || 'Failed to load strategic intelligence'
|
||||
});
|
||||
resolve({ aiInsights: [], aiRecommendations: [] });
|
||||
}
|
||||
},
|
||||
() => {
|
||||
resolve({ aiInsights: [], aiRecommendations: [] });
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch (error: any) {
|
||||
this.updateServiceStatus('aiAnalytics', {
|
||||
status: 'error',
|
||||
progress: 0,
|
||||
message: 'AI analysis failed',
|
||||
message: 'Failed to load AI analytics',
|
||||
error: error.message
|
||||
});
|
||||
return { aiInsights: [], aiRecommendations: [] };
|
||||
|
||||
@@ -192,7 +192,7 @@ interface EnhancedStrategyStore {
|
||||
getPreviousStep: () => ProgressiveDisclosureStep | null;
|
||||
|
||||
// Auto-population actions
|
||||
autoPopulateFromOnboarding: () => Promise<void>;
|
||||
autoPopulateFromOnboarding: (forceRefresh?: boolean) => Promise<void>;
|
||||
updateAutoPopulatedField: (fieldId: string, value: any, source: string) => void;
|
||||
overrideAutoPopulatedField: (fieldId: string, value: any) => void;
|
||||
|
||||
@@ -759,12 +759,21 @@ export const useEnhancedStrategyStore = create<EnhancedStrategyStore>((set, get)
|
||||
},
|
||||
|
||||
// Auto-population actions
|
||||
autoPopulateFromOnboarding: async () => {
|
||||
autoPopulateFromOnboarding: async (forceRefresh: boolean = false) => {
|
||||
set({ loading: true });
|
||||
try {
|
||||
console.log('🔄 Starting auto-population from onboarding data...');
|
||||
|
||||
// This would call the backend to get onboarding data and auto-populate fields
|
||||
// Optionally clear backend caches to force fresh values
|
||||
if (forceRefresh) {
|
||||
try {
|
||||
await contentPlanningApi.clearEnhancedCache(1);
|
||||
console.log('♻️ Cleared enhanced strategy cache for fresh onboarding data');
|
||||
} catch (e) {
|
||||
console.warn('Cache clear failed (non-blocking):', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch onboarding data to auto-populate fields
|
||||
const response = await contentPlanningApi.getOnboardingData();
|
||||
console.log('📡 Backend response:', response);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user