ALwrity version 0.5.4

This commit is contained in:
ajaysi
2025-08-09 23:14:16 +05:30
parent 01fe1e0a9c
commit 5c08b6e007
42 changed files with 3514 additions and 2148 deletions

View File

@@ -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")

View File

@@ -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'
]

View File

@@ -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."""

View File

@@ -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"
}
}

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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

View File

@@ -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 {}

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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),
}

View File

@@ -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

View File

@@ -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'}

View File

@@ -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]")

View File

@@ -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

View File

@@ -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

View File

@@ -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'
]

View File

@@ -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."""

View File

@@ -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'}

View File

@@ -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:

View File

@@ -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

View File

@@ -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)}

View File

@@ -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

View File

@@ -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")

View File

@@ -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.

View File

@@ -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)}

View File

@@ -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."""

View 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=...&region=...` (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 thats 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 35 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=...&region=...` → 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 fields 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

View File

@@ -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"
]
}

View File

@@ -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>

View File

@@ -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 }}
/>
);
})()}

View File

@@ -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>
);

View File

@@ -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) => {

View File

@@ -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: [] };

View File

@@ -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);