feat: image generation overhaul (model-aware text, dim clamping, \.30 pricing), event-driven dashboard cache invalidation, SEO insights (AI visibility, GSC, keyword gap), YouTube OAuth/publish, blog writer & content planning improvements, scheduler monitoring updates
This commit is contained in:
@@ -20,6 +20,9 @@ from ....services.enhanced_strategy_db_service import EnhancedStrategyDBService
|
||||
# Import educational content manager
|
||||
from .content_strategy.educational_content import EducationalContentManager
|
||||
|
||||
# Import authentication
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
# Import utilities
|
||||
from ....utils.error_handlers import ContentPlanningErrorHandler
|
||||
from ....utils.response_builders import ResponseBuilder
|
||||
@@ -40,13 +43,14 @@ _latest_strategies = {}
|
||||
|
||||
@router.post("/generate-comprehensive-strategy")
|
||||
async def generate_comprehensive_strategy(
|
||||
user_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
strategy_name: Optional[str] = None,
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
db: Session = Depends(get_db)
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate a comprehensive AI-powered content strategy."""
|
||||
try:
|
||||
user_id = current_user.get('id')
|
||||
logger.info(f"🚀 Generating comprehensive AI strategy for user: {user_id}")
|
||||
|
||||
# Get user context and onboarding data
|
||||
@@ -103,7 +107,7 @@ async def generate_comprehensive_strategy(
|
||||
|
||||
@router.post("/generate-strategy-component")
|
||||
async def generate_strategy_component(
|
||||
user_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
component_type: str,
|
||||
base_strategy: Optional[Dict[str, Any]] = None,
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
@@ -111,6 +115,7 @@ async def generate_strategy_component(
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate a specific strategy component using AI."""
|
||||
try:
|
||||
user_id = current_user.get('id')
|
||||
logger.info(f"🚀 Generating strategy component '{component_type}' for user: {user_id}")
|
||||
|
||||
# Validate component type
|
||||
@@ -187,11 +192,12 @@ async def generate_strategy_component(
|
||||
|
||||
@router.get("/strategy-generation-status")
|
||||
async def get_strategy_generation_status(
|
||||
user_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> Dict[str, Any]:
|
||||
"""Get the status of strategy generation for a user."""
|
||||
try:
|
||||
user_id = current_user.get('id')
|
||||
logger.info(f"Getting strategy generation status for user: {user_id}")
|
||||
|
||||
# Get user's strategies
|
||||
@@ -247,6 +253,7 @@ async def get_strategy_generation_status(
|
||||
async def optimize_existing_strategy(
|
||||
strategy_id: int,
|
||||
optimization_type: str = "comprehensive",
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> Dict[str, Any]:
|
||||
"""Optimize an existing strategy using AI."""
|
||||
@@ -309,12 +316,13 @@ async def optimize_existing_strategy(
|
||||
@router.post("/generate-comprehensive-strategy-polling")
|
||||
async def generate_comprehensive_strategy_polling(
|
||||
request: Dict[str, Any],
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate a comprehensive AI-powered content strategy using polling approach."""
|
||||
try:
|
||||
# Extract parameters from request body
|
||||
user_id = request.get("user_id", 1)
|
||||
user_id = current_user.get('id')
|
||||
strategy_name = request.get("strategy_name")
|
||||
config = request.get("config", {})
|
||||
|
||||
@@ -611,6 +619,7 @@ async def generate_comprehensive_strategy_polling(
|
||||
@router.get("/strategy-generation-status/{task_id}")
|
||||
async def get_strategy_generation_status_by_task(
|
||||
task_id: str,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> Dict[str, Any]:
|
||||
"""Get the status of strategy generation for a specific task."""
|
||||
@@ -647,11 +656,12 @@ async def get_strategy_generation_status_by_task(
|
||||
|
||||
@router.get("/latest-strategy")
|
||||
async def get_latest_generated_strategy(
|
||||
user_id: int = Query(1, description="User ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> Dict[str, Any]:
|
||||
"""Get the latest generated strategy from the polling system or database."""
|
||||
try:
|
||||
user_id = current_user.get('id')
|
||||
logger.info(f"🔍 Getting latest generated strategy for user: {user_id}")
|
||||
|
||||
# First, try to get from database (most reliable)
|
||||
|
||||
@@ -19,6 +19,9 @@ from ....services.enhanced_strategy_db_service import EnhancedStrategyDBService
|
||||
# Import models
|
||||
from models.enhanced_strategy_models import EnhancedContentStrategy, EnhancedAIAnalysisResult
|
||||
|
||||
# Import authentication
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
# Import utilities
|
||||
from ....utils.error_handlers import ContentPlanningErrorHandler
|
||||
from ....utils.response_builders import ResponseBuilder
|
||||
@@ -37,6 +40,7 @@ def get_db():
|
||||
@router.get("/{strategy_id}/analytics")
|
||||
async def get_enhanced_strategy_analytics(
|
||||
strategy_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> Dict[str, Any]:
|
||||
"""Get comprehensive analytics for an enhanced strategy."""
|
||||
@@ -72,6 +76,7 @@ async def get_enhanced_strategy_analytics(
|
||||
async def get_enhanced_strategy_ai_analysis(
|
||||
strategy_id: int,
|
||||
limit: int = Query(10, description="Number of AI analysis results to return"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> Dict[str, Any]:
|
||||
"""Get AI analysis history for an enhanced strategy."""
|
||||
@@ -108,6 +113,7 @@ async def get_enhanced_strategy_ai_analysis(
|
||||
@router.get("/{strategy_id}/completion")
|
||||
async def get_enhanced_strategy_completion_stats(
|
||||
strategy_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> Dict[str, Any]:
|
||||
"""Get completion statistics for an enhanced strategy."""
|
||||
@@ -147,6 +153,7 @@ async def get_enhanced_strategy_completion_stats(
|
||||
@router.get("/{strategy_id}/onboarding-integration")
|
||||
async def get_enhanced_strategy_onboarding_integration(
|
||||
strategy_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> Dict[str, Any]:
|
||||
"""Get onboarding data integration for an enhanced strategy."""
|
||||
@@ -177,6 +184,7 @@ async def get_enhanced_strategy_onboarding_integration(
|
||||
@router.post("/{strategy_id}/ai-recommendations")
|
||||
async def generate_enhanced_ai_recommendations(
|
||||
strategy_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate AI recommendations for an enhanced strategy."""
|
||||
@@ -216,6 +224,7 @@ async def generate_enhanced_ai_recommendations(
|
||||
async def regenerate_enhanced_strategy_ai_analysis(
|
||||
strategy_id: int,
|
||||
analysis_type: str,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> Dict[str, Any]:
|
||||
"""Regenerate AI analysis for an enhanced strategy."""
|
||||
|
||||
@@ -21,6 +21,9 @@ 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 authentication
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
# Import utilities
|
||||
from ....utils.error_handlers import ContentPlanningErrorHandler
|
||||
from ....utils.response_builders import ResponseBuilder
|
||||
@@ -49,12 +52,13 @@ async def stream_data(data_generator):
|
||||
async def accept_autofill_inputs(
|
||||
strategy_id: int,
|
||||
payload: Dict[str, Any],
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
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 = str(payload.get('user_id') or "")
|
||||
user_id = str(current_user.get('id'))
|
||||
accepted_fields = payload.get('accepted_fields') or {}
|
||||
# Optional transparency bundles
|
||||
sources = payload.get('sources') or {}
|
||||
@@ -99,7 +103,7 @@ async def 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"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
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)
|
||||
@@ -107,7 +111,7 @@ async def stream_autofill_refresh(
|
||||
"""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
|
||||
actual_user_id = current_user.get('id', 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}
|
||||
@@ -203,14 +207,14 @@ async def stream_autofill_refresh(
|
||||
|
||||
@router.post("/autofill/refresh")
|
||||
async def refresh_autofill(
|
||||
user_id: Optional[int] = Query(None, description="User ID to build auto-fill for"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
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
|
||||
actual_user_id = current_user.get('id', 1)
|
||||
started = datetime.utcnow()
|
||||
refresh_service = AutoFillRefreshService(db)
|
||||
payload = await refresh_service.build_fresh_payload_with_transparency(actual_user_id, use_ai=use_ai, ai_only=ai_only)
|
||||
|
||||
@@ -4,7 +4,7 @@ Handles streaming endpoints for enhanced content strategies.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from starlette.requests import Request
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -12,8 +12,6 @@ from loguru import logger
|
||||
import json
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
import time
|
||||
|
||||
# Import database
|
||||
from services.database import get_db_session
|
||||
@@ -25,31 +23,13 @@ from middleware.auth_middleware import get_current_user, get_current_user_with_q
|
||||
from ....services.enhanced_strategy_service import EnhancedStrategyService
|
||||
from ....services.enhanced_strategy_db_service import EnhancedStrategyDBService
|
||||
|
||||
# Import utilities
|
||||
from ....utils.error_handlers import ContentPlanningErrorHandler
|
||||
from ....utils.response_builders import ResponseBuilder
|
||||
from ....utils.constants import ERROR_MESSAGES, SUCCESS_MESSAGES
|
||||
# Use bounded shared cache instead of process-local unbounded dict
|
||||
from ...services.content_strategy.performance.caching import CachingService
|
||||
|
||||
router = APIRouter(tags=["Strategy Streaming"])
|
||||
|
||||
# Cache for streaming endpoints (5 minutes cache)
|
||||
streaming_cache = defaultdict(dict)
|
||||
CACHE_DURATION = 300 # 5 minutes
|
||||
|
||||
def get_cached_data(cache_key: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get cached data if it exists and is not expired."""
|
||||
if cache_key in streaming_cache:
|
||||
cached_data = streaming_cache[cache_key]
|
||||
if time.time() - cached_data.get("timestamp", 0) < CACHE_DURATION:
|
||||
return cached_data.get("data")
|
||||
return None
|
||||
|
||||
def set_cached_data(cache_key: str, data: Dict[str, Any]):
|
||||
"""Set cached data with timestamp."""
|
||||
streaming_cache[cache_key] = {
|
||||
"data": data,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
# Shared bounded cache for streaming endpoints
|
||||
streaming_cache_service = CachingService()
|
||||
|
||||
# Helper function to get database session
|
||||
def get_db():
|
||||
@@ -123,11 +103,7 @@ async def stream_enhanced_strategies(
|
||||
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"
|
||||
"Connection": "keep-alive"
|
||||
}
|
||||
)
|
||||
|
||||
@@ -150,9 +126,9 @@ async def stream_strategic_intelligence(
|
||||
|
||||
logger.info(f"🚀 Starting strategic intelligence stream for authenticated user: {authenticated_user_id}")
|
||||
|
||||
# Check cache first
|
||||
# Check bounded shared cache first
|
||||
cache_key = f"strategic_intelligence_{authenticated_user_id}"
|
||||
cached_data = get_cached_data(cache_key)
|
||||
cached_data = await streaming_cache_service.get_cached_data("streaming_intelligence", cache_key)
|
||||
if cached_data:
|
||||
logger.info(f"✅ Returning cached strategic intelligence data for user: {authenticated_user_id}")
|
||||
yield {"type": "result", "status": "success", "data": cached_data, "progress": 100}
|
||||
@@ -167,7 +143,6 @@ async def stream_strategic_intelligence(
|
||||
# Send progress update
|
||||
yield {"type": "progress", "message": "Retrieving strategies...", "progress": 20}
|
||||
|
||||
# Use authenticated user_id to ensure users can only see their own strategies
|
||||
strategies_data = await enhanced_service.get_enhanced_strategies(authenticated_user_id, None, db)
|
||||
|
||||
# Send progress update
|
||||
@@ -194,54 +169,29 @@ async def stream_strategic_intelligence(
|
||||
# Send progress update
|
||||
yield {"type": "progress", "message": "Processing intelligence data...", "progress": 60}
|
||||
|
||||
# Build strategic intelligence from actual strategy data — no hardcoded fallback defaults
|
||||
strategic_intelligence = {
|
||||
"market_positioning": {
|
||||
"current_position": strategy.get("competitive_position", "Challenger"),
|
||||
"target_position": "Market Leader",
|
||||
"differentiation_factors": [
|
||||
"AI-powered content optimization",
|
||||
"Data-driven strategy development",
|
||||
"Personalized user experience"
|
||||
]
|
||||
"current_position": strategy.get("competitive_position") or None,
|
||||
"differentiation_factors": strategy.get("differentiation_factors") or None
|
||||
},
|
||||
"competitive_analysis": {
|
||||
"top_competitors": strategy.get("top_competitors", [])[:3] or [
|
||||
"Competitor A", "Competitor B", "Competitor C"
|
||||
],
|
||||
"competitive_advantages": [
|
||||
"Advanced AI capabilities",
|
||||
"Comprehensive data integration",
|
||||
"User-centric design"
|
||||
],
|
||||
"market_gaps": strategy.get("market_gaps", []) or [
|
||||
"AI-driven content personalization",
|
||||
"Real-time performance optimization",
|
||||
"Predictive analytics"
|
||||
]
|
||||
"top_competitors": (strategy.get("top_competitors") or [None])[:3],
|
||||
"competitive_advantages": strategy.get("competitive_advantages") or None,
|
||||
"market_gaps": strategy.get("market_gaps") or None
|
||||
},
|
||||
"ai_insights": ai_recommendations.get("strategic_insights", []) or [
|
||||
"Focus on pillar content strategy",
|
||||
"Implement topic clustering",
|
||||
"Optimize for voice search"
|
||||
],
|
||||
"opportunities": [
|
||||
{
|
||||
"area": "Content Personalization",
|
||||
"potential_impact": "High",
|
||||
"implementation_timeline": "3-6 months",
|
||||
"estimated_roi": "25-40%"
|
||||
},
|
||||
{
|
||||
"area": "AI-Powered Optimization",
|
||||
"potential_impact": "Medium",
|
||||
"implementation_timeline": "6-12 months",
|
||||
"estimated_roi": "15-30%"
|
||||
}
|
||||
]
|
||||
"ai_insights": ai_recommendations.get("strategic_insights") if ai_recommendations else None,
|
||||
"opportunities": strategy.get("opportunities") or None
|
||||
}
|
||||
|
||||
# Filter out null-only sections for cleaner responses
|
||||
strategic_intelligence = {
|
||||
k: v for k, v in strategic_intelligence.items()
|
||||
if v is not None and v != [None]
|
||||
}
|
||||
|
||||
# Cache the strategic intelligence data
|
||||
set_cached_data(cache_key, strategic_intelligence)
|
||||
await streaming_cache_service.set_cached_data("streaming_intelligence", cache_key, strategic_intelligence)
|
||||
|
||||
# Send progress update
|
||||
yield {"type": "progress", "message": "Finalizing strategic intelligence...", "progress": 80}
|
||||
@@ -260,11 +210,7 @@ async def stream_strategic_intelligence(
|
||||
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"
|
||||
"Connection": "keep-alive"
|
||||
}
|
||||
)
|
||||
|
||||
@@ -287,9 +233,9 @@ async def stream_keyword_research(
|
||||
|
||||
logger.info(f"🚀 Starting keyword research stream for authenticated user: {authenticated_user_id}")
|
||||
|
||||
# Check cache first
|
||||
# Check bounded shared cache first
|
||||
cache_key = f"keyword_research_{authenticated_user_id}"
|
||||
cached_data = get_cached_data(cache_key)
|
||||
cached_data = await streaming_cache_service.get_cached_data("streaming_intelligence", cache_key)
|
||||
if cached_data:
|
||||
logger.info(f"✅ Returning cached keyword research data for user: {authenticated_user_id}")
|
||||
yield {"type": "result", "status": "success", "data": cached_data, "progress": 100}
|
||||
@@ -333,33 +279,24 @@ async def stream_keyword_research(
|
||||
# Send progress update
|
||||
yield {"type": "progress", "message": "Processing keyword data...", "progress": 60}
|
||||
|
||||
# Build keyword data from actual analysis — no hardcoded fallback defaults
|
||||
keyword_data = {
|
||||
"trend_analysis": {
|
||||
"high_volume_keywords": analysis_results.get("opportunities", [])[:3] or [
|
||||
{"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"}
|
||||
]
|
||||
"high_volume_keywords": (analysis_results.get("opportunities") or [None])[:3],
|
||||
"trending_keywords": analysis_results.get("trending_keywords") or None
|
||||
},
|
||||
"intent_analysis": {
|
||||
"informational": ["how to", "what is", "guide to"],
|
||||
"navigational": ["company name", "brand name", "website"],
|
||||
"transactional": ["buy", "purchase", "download", "sign up"]
|
||||
},
|
||||
"opportunities": analysis_results.get("opportunities", []) or [
|
||||
{"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"}
|
||||
]
|
||||
"intent_analysis": analysis_results.get("intent_analysis") or None,
|
||||
"opportunities": analysis_results.get("opportunities") or None
|
||||
}
|
||||
|
||||
# Filter out null-only sections
|
||||
keyword_data = {
|
||||
k: v for k, v in keyword_data.items()
|
||||
if v is not None and v != [None]
|
||||
}
|
||||
|
||||
# Cache the keyword data
|
||||
set_cached_data(cache_key, keyword_data)
|
||||
await streaming_cache_service.set_cached_data("streaming_intelligence", cache_key, keyword_data)
|
||||
|
||||
# Send progress update
|
||||
yield {"type": "progress", "message": "Finalizing keyword research...", "progress": 80}
|
||||
@@ -378,10 +315,71 @@ async def stream_keyword_research(
|
||||
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"
|
||||
"Connection": "keep-alive"
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@router.get("/stream/ai-generation-status")
|
||||
async def stream_ai_generation_status(
|
||||
request: Request,
|
||||
strategy_id: int = Query(..., description="Strategy ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_with_query_token),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Stream AI generation status for a strategy with real-time updates."""
|
||||
|
||||
async def status_generator():
|
||||
try:
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
if not clerk_user_id:
|
||||
yield {"type": "error", "detail": "Invalid user ID", "progress": 0}
|
||||
return
|
||||
|
||||
authenticated_user_id = clerk_user_id
|
||||
|
||||
logger.info(f"🚀 Starting AI generation status stream for user: {authenticated_user_id}, strategy: {strategy_id}")
|
||||
|
||||
yield {"type": "progress", "detail": "Fetching AI generation status...", "progress": 10}
|
||||
|
||||
db_service = EnhancedStrategyDBService(db)
|
||||
enhanced_service = EnhancedStrategyService(db_service)
|
||||
|
||||
strategy = await enhanced_service.get_enhanced_strategy(strategy_id, authenticated_user_id, db)
|
||||
|
||||
if not strategy or strategy.get("status") == "not_found":
|
||||
yield {"type": "error", "detail": "Strategy not found", "progress": 0}
|
||||
return
|
||||
|
||||
yield {"type": "progress", "detail": "Checking AI analysis status...", "progress": 30}
|
||||
|
||||
ai_recommendations = strategy.get("ai_recommendations")
|
||||
if ai_recommendations:
|
||||
if isinstance(ai_recommendations, str):
|
||||
try:
|
||||
ai_recommendations = json.loads(ai_recommendations)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
ai_recommendations = {}
|
||||
|
||||
ai_status = "completed" if ai_recommendations else "pending"
|
||||
|
||||
if ai_status == "completed":
|
||||
yield {"type": "progress", "detail": "AI analysis completed", "progress": 80}
|
||||
yield {"type": "result", "status": "completed", "detail": "AI generation completed", "progress": 100}
|
||||
else:
|
||||
yield {"type": "progress", "detail": "AI analysis is pending", "progress": 50}
|
||||
yield {"type": "result", "status": "pending", "detail": "AI generation is in progress", "progress": 50}
|
||||
|
||||
logger.info(f"✅ AI generation status stream completed for user: {authenticated_user_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error in AI generation status stream: {str(e)}")
|
||||
yield {"type": "error", "detail": str(e), "progress": 0}
|
||||
|
||||
return StreamingResponse(
|
||||
stream_data(status_generator()),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive"
|
||||
}
|
||||
)
|
||||
|
||||
@@ -65,12 +65,16 @@ async def analyze_content_evolution(
|
||||
)
|
||||
|
||||
@router.post("/performance-trends", response_model=AIAnalyticsResponse)
|
||||
async def analyze_performance_trends(request: PerformanceTrendsRequest):
|
||||
async def analyze_performance_trends(
|
||||
request: PerformanceTrendsRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Analyze performance trends for content strategy.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Starting performance trends analysis for strategy {request.strategy_id}")
|
||||
user_id = current_user.get("user_id")
|
||||
logger.info(f"Starting performance trends analysis for strategy {request.strategy_id} (user {user_id})")
|
||||
|
||||
result = await ai_analytics_service.analyze_performance_trends(
|
||||
strategy_id=request.strategy_id,
|
||||
@@ -87,12 +91,16 @@ async def analyze_performance_trends(request: PerformanceTrendsRequest):
|
||||
)
|
||||
|
||||
@router.post("/predict-performance", response_model=AIAnalyticsResponse)
|
||||
async def predict_content_performance(request: ContentPerformancePredictionRequest):
|
||||
async def predict_content_performance(
|
||||
request: ContentPerformancePredictionRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Predict content performance using AI models.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Starting content performance prediction for strategy {request.strategy_id}")
|
||||
user_id = current_user.get("user_id")
|
||||
logger.info(f"Starting content performance prediction for strategy {request.strategy_id} (user {user_id})")
|
||||
|
||||
result = await ai_analytics_service.predict_content_performance(
|
||||
strategy_id=request.strategy_id,
|
||||
@@ -137,12 +145,13 @@ async def generate_strategic_intelligence(
|
||||
|
||||
@router.get("/", response_model=Dict[str, Any])
|
||||
async def get_ai_analytics(
|
||||
user_id: Optional[int] = Query(None, description="User ID"),
|
||||
strategy_id: Optional[int] = Query(None, description="Strategy ID"),
|
||||
force_refresh: bool = Query(False, description="Force refresh AI analysis")
|
||||
force_refresh: bool = Query(False, description="Force refresh AI analysis"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Get AI analytics with real personalized insights - Database first approach."""
|
||||
try:
|
||||
user_id = current_user.get("user_id") or current_user.get("id")
|
||||
logger.info(f"🚀 Starting AI analytics for user: {user_id}, strategy: {strategy_id}, force_refresh: {force_refresh}")
|
||||
|
||||
result = await ai_analytics_service.get_ai_analytics(user_id, strategy_id, force_refresh)
|
||||
@@ -153,11 +162,14 @@ async def get_ai_analytics(
|
||||
raise HTTPException(status_code=500, detail=f"Error generating AI analytics: {str(e)}")
|
||||
|
||||
@router.get("/health")
|
||||
async def ai_analytics_health_check():
|
||||
async def ai_analytics_health_check(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Health check for AI analytics services.
|
||||
"""
|
||||
try:
|
||||
logger.debug(f"AI analytics health check by user: {current_user.get('id')}")
|
||||
# Check AI analytics service
|
||||
service_status = {}
|
||||
|
||||
@@ -197,14 +209,16 @@ async def ai_analytics_health_check():
|
||||
async def get_user_ai_analysis_results(
|
||||
user_id: int,
|
||||
analysis_type: Optional[str] = Query(None, description="Filter by analysis type"),
|
||||
limit: int = Query(10, description="Number of results to return")
|
||||
limit: int = Query(10, description="Number of results to return"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Get AI analysis results for a specific user."""
|
||||
"""Get AI analysis results for the authenticated user."""
|
||||
try:
|
||||
logger.info(f"Fetching AI analysis results for user {user_id}")
|
||||
authenticated_user_id = current_user.get("user_id") or current_user.get("id")
|
||||
logger.info(f"Fetching AI analysis results for authenticated user {authenticated_user_id}")
|
||||
|
||||
result = await ai_analytics_service.get_user_ai_analysis_results(
|
||||
user_id=user_id,
|
||||
user_id=authenticated_user_id,
|
||||
analysis_type=analysis_type,
|
||||
limit=limit
|
||||
)
|
||||
@@ -219,14 +233,16 @@ async def get_user_ai_analysis_results(
|
||||
async def refresh_ai_analysis(
|
||||
user_id: int,
|
||||
analysis_type: str = Query(..., description="Type of analysis to refresh"),
|
||||
strategy_id: Optional[int] = Query(None, description="Strategy ID")
|
||||
strategy_id: Optional[int] = Query(None, description="Strategy ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Force refresh of AI analysis for a user."""
|
||||
"""Force refresh of AI analysis for the authenticated user."""
|
||||
try:
|
||||
logger.info(f"Force refreshing AI analysis for user {user_id}, type: {analysis_type}")
|
||||
authenticated_user_id = current_user.get("user_id") or current_user.get("id")
|
||||
logger.info(f"Force refreshing AI analysis for authenticated user {authenticated_user_id}, type: {analysis_type}")
|
||||
|
||||
result = await ai_analytics_service.refresh_ai_analysis(
|
||||
user_id=user_id,
|
||||
user_id=authenticated_user_id,
|
||||
analysis_type=analysis_type,
|
||||
strategy_id=strategy_id
|
||||
)
|
||||
@@ -240,14 +256,16 @@ async def refresh_ai_analysis(
|
||||
@router.delete("/cache/{user_id}")
|
||||
async def clear_ai_analysis_cache(
|
||||
user_id: int,
|
||||
analysis_type: Optional[str] = Query(None, description="Specific analysis type to clear")
|
||||
analysis_type: Optional[str] = Query(None, description="Specific analysis type to clear"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Clear AI analysis cache for a user."""
|
||||
"""Clear AI analysis cache for the authenticated user."""
|
||||
try:
|
||||
logger.info(f"Clearing AI analysis cache for user {user_id}")
|
||||
authenticated_user_id = current_user.get("user_id") or current_user.get("id")
|
||||
logger.info(f"Clearing AI analysis cache for authenticated user {authenticated_user_id}")
|
||||
|
||||
result = await ai_analytics_service.clear_ai_analysis_cache(
|
||||
user_id=user_id,
|
||||
user_id=authenticated_user_id,
|
||||
analysis_type=analysis_type
|
||||
)
|
||||
|
||||
@@ -259,13 +277,15 @@ async def clear_ai_analysis_cache(
|
||||
|
||||
@router.get("/statistics")
|
||||
async def get_ai_analysis_statistics(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
user_id: Optional[int] = Query(None, description="User ID for user-specific stats")
|
||||
):
|
||||
"""Get AI analysis statistics."""
|
||||
try:
|
||||
logger.info(f"📊 Getting AI analysis statistics for user: {user_id}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"📊 Getting AI analysis statistics for authenticated user: {clerk_user_id}")
|
||||
|
||||
result = await ai_analytics_service.get_ai_analysis_statistics(user_id)
|
||||
result = await ai_analytics_service.get_ai_analysis_statistics(user_id or clerk_user_id)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -9,6 +9,9 @@ from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
|
||||
# Import authentication
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
# Import database service
|
||||
from services.database import get_db_session, get_db
|
||||
from services.content_planning_db import ContentPlanningDBService
|
||||
@@ -34,13 +37,16 @@ router = APIRouter(prefix="/calendar-events", tags=["calendar-events"])
|
||||
@router.post("/", response_model=CalendarEventResponse)
|
||||
async def create_calendar_event(
|
||||
event: CalendarEventCreate,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new calendar event."""
|
||||
try:
|
||||
logger.info(f"Creating calendar event: {event.title}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Creating calendar event: {event.title} for user: {clerk_user_id}")
|
||||
|
||||
event_data = event.dict()
|
||||
event_data['user_id'] = clerk_user_id
|
||||
created_event = await calendar_service.create_calendar_event(event_data, db)
|
||||
|
||||
return CalendarEventResponse(**created_event)
|
||||
@@ -54,11 +60,13 @@ async def create_calendar_event(
|
||||
@router.get("/", response_model=List[CalendarEventResponse])
|
||||
async def get_calendar_events(
|
||||
strategy_id: Optional[int] = Query(None, description="Filter by strategy ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get calendar events, optionally filtered by strategy."""
|
||||
try:
|
||||
logger.info("Fetching calendar events")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Fetching calendar events for user: {clerk_user_id}")
|
||||
|
||||
events = await calendar_service.get_calendar_events(strategy_id, db)
|
||||
return [CalendarEventResponse(**event) for event in events]
|
||||
@@ -70,11 +78,13 @@ async def get_calendar_events(
|
||||
@router.get("/{event_id}", response_model=CalendarEventResponse)
|
||||
async def get_calendar_event(
|
||||
event_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get a specific calendar event by ID."""
|
||||
try:
|
||||
logger.info(f"Fetching calendar event: {event_id}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Fetching calendar event: {event_id} for user: {clerk_user_id}")
|
||||
|
||||
event = await calendar_service.get_calendar_event_by_id(event_id, db)
|
||||
return CalendarEventResponse(**event)
|
||||
@@ -89,11 +99,13 @@ async def get_calendar_event(
|
||||
async def update_calendar_event(
|
||||
event_id: int,
|
||||
update_data: Dict[str, Any],
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update a calendar event."""
|
||||
try:
|
||||
logger.info(f"Updating calendar event: {event_id}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Updating calendar event: {event_id} for user: {clerk_user_id}")
|
||||
|
||||
updated_event = await calendar_service.update_calendar_event(event_id, update_data, db)
|
||||
return CalendarEventResponse(**updated_event)
|
||||
@@ -107,11 +119,13 @@ async def update_calendar_event(
|
||||
@router.delete("/{event_id}")
|
||||
async def delete_calendar_event(
|
||||
event_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Delete a calendar event."""
|
||||
try:
|
||||
logger.info(f"Deleting calendar event: {event_id}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Deleting calendar event: {event_id} for user: {clerk_user_id}")
|
||||
|
||||
deleted = await calendar_service.delete_calendar_event(event_id, db)
|
||||
|
||||
@@ -129,11 +143,13 @@ async def delete_calendar_event(
|
||||
@router.post("/schedule", response_model=Dict[str, Any])
|
||||
async def schedule_calendar_event(
|
||||
event: CalendarEventCreate,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Schedule a calendar event with conflict checking."""
|
||||
try:
|
||||
logger.info(f"Scheduling calendar event: {event.title}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Scheduling calendar event: {event.title} for user: {clerk_user_id}")
|
||||
|
||||
event_data = event.dict()
|
||||
result = await calendar_service.schedule_event(event_data, db)
|
||||
@@ -147,11 +163,13 @@ async def schedule_calendar_event(
|
||||
async def get_strategy_events(
|
||||
strategy_id: int,
|
||||
status: Optional[str] = Query(None, description="Filter by event status"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get calendar events for a specific strategy."""
|
||||
try:
|
||||
logger.info(f"Fetching events for strategy: {strategy_id}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Fetching events for strategy: {strategy_id} for user: {clerk_user_id}")
|
||||
|
||||
if status:
|
||||
events = await calendar_service.get_events_by_status(strategy_id, status, db)
|
||||
|
||||
@@ -114,25 +114,23 @@ async def generate_comprehensive_calendar(
|
||||
)
|
||||
|
||||
@router.post("/optimize-content", response_model=ContentOptimizationResponse)
|
||||
async def optimize_content_for_platform(request: ContentOptimizationRequest, db: Session = Depends(get_db)):
|
||||
async def optimize_content_for_platform(
|
||||
request: ContentOptimizationRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Optimize content for specific platforms using database insights.
|
||||
|
||||
This endpoint optimizes content based on:
|
||||
- Historical performance data for the platform
|
||||
- Audience preferences from onboarding data
|
||||
- Gap analysis insights for content improvement
|
||||
- Competitor analysis for differentiation
|
||||
- Active strategy data for optimal alignment
|
||||
Optimize content for specific platforms using database insights with user isolation.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"🔧 Starting content optimization for user {request.user_id}")
|
||||
clerk_user_id = str(current_user.get('id'))
|
||||
logger.info(f"🔧 Starting content optimization for authenticated user {clerk_user_id}")
|
||||
|
||||
# Initialize service with database session for active strategy access
|
||||
calendar_service = CalendarGenerationService(db)
|
||||
|
||||
result = await calendar_service.optimize_content_for_platform(
|
||||
user_id=request.user_id,
|
||||
user_id=clerk_user_id,
|
||||
title=request.title,
|
||||
description=request.description,
|
||||
content_type=request.content_type,
|
||||
@@ -152,24 +150,23 @@ async def optimize_content_for_platform(request: ContentOptimizationRequest, db:
|
||||
)
|
||||
|
||||
@router.post("/performance-predictions", response_model=PerformancePredictionResponse)
|
||||
async def predict_content_performance(request: PerformancePredictionRequest, db: Session = Depends(get_db)):
|
||||
async def predict_content_performance(
|
||||
request: PerformancePredictionRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Predict content performance using database insights.
|
||||
|
||||
This endpoint predicts performance based on:
|
||||
- Historical performance data
|
||||
- Audience demographics and preferences
|
||||
- Content type and platform patterns
|
||||
- Gap analysis opportunities
|
||||
Predict content performance using database insights with user isolation.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"📊 Starting performance prediction for user {request.user_id}")
|
||||
clerk_user_id = str(current_user.get('id'))
|
||||
logger.info(f"📊 Starting performance prediction for authenticated user {clerk_user_id}")
|
||||
|
||||
# Initialize service with database session for active strategy access
|
||||
calendar_service = CalendarGenerationService(db)
|
||||
|
||||
result = await calendar_service.predict_content_performance(
|
||||
user_id=request.user_id,
|
||||
user_id=clerk_user_id,
|
||||
content_type=request.content_type,
|
||||
platform=request.platform,
|
||||
content_data=request.content_data,
|
||||
@@ -186,24 +183,23 @@ async def predict_content_performance(request: PerformancePredictionRequest, db:
|
||||
)
|
||||
|
||||
@router.post("/repurpose-content", response_model=ContentRepurposingResponse)
|
||||
async def repurpose_content_across_platforms(request: ContentRepurposingRequest, db: Session = Depends(get_db)):
|
||||
async def repurpose_content_across_platforms(
|
||||
request: ContentRepurposingRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Repurpose content across different platforms using database insights.
|
||||
|
||||
This endpoint suggests content repurposing based on:
|
||||
- Existing content and strategy data
|
||||
- Gap analysis opportunities
|
||||
- Platform-specific requirements
|
||||
- Audience preferences
|
||||
Repurpose content across different platforms using database insights with user isolation.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"🔄 Starting content repurposing for user {request.user_id}")
|
||||
clerk_user_id = str(current_user.get('id'))
|
||||
logger.info(f"🔄 Starting content repurposing for authenticated user {clerk_user_id}")
|
||||
|
||||
# Initialize service with database session for active strategy access
|
||||
calendar_service = CalendarGenerationService(db)
|
||||
|
||||
result = await calendar_service.repurpose_content_across_platforms(
|
||||
user_id=request.user_id,
|
||||
user_id=clerk_user_id,
|
||||
original_content=request.original_content,
|
||||
target_platforms=request.target_platforms,
|
||||
strategy_id=request.strategy_id
|
||||
@@ -312,12 +308,16 @@ async def get_comprehensive_user_data(
|
||||
)
|
||||
|
||||
@router.get("/health")
|
||||
async def calendar_generation_health_check(db: Session = Depends(get_db)):
|
||||
async def calendar_generation_health_check(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Health check for calendar generation services.
|
||||
"""
|
||||
try:
|
||||
logger.info("🏥 Performing calendar generation health check")
|
||||
clerk_user_id = str(current_user.get('id'))
|
||||
logger.info(f"🏥 Performing calendar generation health check for user {clerk_user_id}")
|
||||
|
||||
# Initialize service with database session for active strategy access
|
||||
calendar_service = CalendarGenerationService(db)
|
||||
@@ -337,12 +337,17 @@ async def calendar_generation_health_check(db: Session = Depends(get_db)):
|
||||
}
|
||||
|
||||
@router.get("/progress/{session_id}")
|
||||
async def get_calendar_generation_progress(session_id: str, db: Session = Depends(get_db)):
|
||||
async def get_calendar_generation_progress(
|
||||
session_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get real-time progress of calendar generation for a specific session.
|
||||
This endpoint is polled by the frontend modal to show progress updates.
|
||||
"""
|
||||
try:
|
||||
clerk_user_id = str(current_user.get('id'))
|
||||
# Initialize service with database session for active strategy access
|
||||
calendar_service = CalendarGenerationService(db)
|
||||
|
||||
@@ -433,11 +438,16 @@ async def start_calendar_generation(
|
||||
raise HTTPException(status_code=500, detail="Failed to start calendar generation")
|
||||
|
||||
@router.delete("/cancel/{session_id}")
|
||||
async def cancel_calendar_generation(session_id: str, db: Session = Depends(get_db)):
|
||||
async def cancel_calendar_generation(
|
||||
session_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Cancel an ongoing calendar generation session.
|
||||
"""
|
||||
try:
|
||||
clerk_user_id = str(current_user.get('id'))
|
||||
# Initialize service with database session for active strategy access
|
||||
calendar_service = CalendarGenerationService(db)
|
||||
|
||||
@@ -463,9 +473,13 @@ async def cancel_calendar_generation(session_id: str, db: Session = Depends(get_
|
||||
|
||||
# Cache Management Endpoints
|
||||
@router.get("/cache/stats")
|
||||
async def get_cache_stats(db: Session = Depends(get_db)) -> Dict[str, Any]:
|
||||
async def get_cache_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> Dict[str, Any]:
|
||||
"""Get comprehensive user data cache statistics."""
|
||||
try:
|
||||
clerk_user_id = str(current_user.get('id'))
|
||||
from services.comprehensive_user_data_cache_service import ComprehensiveUserDataCacheService
|
||||
cache_service = ComprehensiveUserDataCacheService(db)
|
||||
stats = cache_service.get_cache_stats()
|
||||
@@ -478,19 +492,21 @@ async def get_cache_stats(db: Session = Depends(get_db)) -> Dict[str, Any]:
|
||||
async def invalidate_user_cache(
|
||||
user_id: str,
|
||||
strategy_id: Optional[int] = Query(None, description="Strategy ID to invalidate (optional)"),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> Dict[str, Any]:
|
||||
"""Invalidate cache for a specific user/strategy."""
|
||||
"""Invalidate cache for the authenticated user."""
|
||||
try:
|
||||
clerk_user_id = str(current_user.get('id'))
|
||||
from services.comprehensive_user_data_cache_service import ComprehensiveUserDataCacheService
|
||||
cache_service = ComprehensiveUserDataCacheService(db)
|
||||
success = cache_service.invalidate_cache(user_id, strategy_id)
|
||||
success = cache_service.invalidate_cache(clerk_user_id, strategy_id)
|
||||
|
||||
if success:
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Cache invalidated for user {user_id}" + (f" and strategy {strategy_id}" if strategy_id else ""),
|
||||
"user_id": user_id,
|
||||
"message": f"Cache invalidated for user {clerk_user_id}" + (f" and strategy {strategy_id}" if strategy_id else ""),
|
||||
"user_id": clerk_user_id,
|
||||
"strategy_id": strategy_id
|
||||
}
|
||||
else:
|
||||
@@ -501,9 +517,13 @@ async def invalidate_user_cache(
|
||||
raise HTTPException(status_code=500, detail="Failed to invalidate cache")
|
||||
|
||||
@router.post("/cache/cleanup")
|
||||
async def cleanup_expired_cache(db: Session = Depends(get_db)) -> Dict[str, Any]:
|
||||
async def cleanup_expired_cache(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> Dict[str, Any]:
|
||||
"""Clean up expired cache entries."""
|
||||
try:
|
||||
clerk_user_id = str(current_user.get('id'))
|
||||
from services.comprehensive_user_data_cache_service import ComprehensiveUserDataCacheService
|
||||
cache_service = ComprehensiveUserDataCacheService(db)
|
||||
deleted_count = cache_service.cleanup_expired_cache()
|
||||
@@ -519,16 +539,22 @@ async def cleanup_expired_cache(db: Session = Depends(get_db)) -> Dict[str, Any]
|
||||
raise HTTPException(status_code=500, detail="Failed to clean up cache")
|
||||
|
||||
@router.get("/sessions")
|
||||
async def list_active_sessions(db: Session = Depends(get_db)):
|
||||
async def list_active_sessions(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
List all active calendar generation sessions.
|
||||
List active calendar generation sessions for the authenticated user.
|
||||
"""
|
||||
try:
|
||||
clerk_user_id = str(current_user.get('id'))
|
||||
# Initialize service with database session for active strategy access
|
||||
calendar_service = CalendarGenerationService(db)
|
||||
|
||||
sessions = []
|
||||
for session_id, session_data in calendar_service.orchestrator_sessions.items():
|
||||
if str(session_data.get("user_id", "")) != clerk_user_id:
|
||||
continue
|
||||
sessions.append({
|
||||
"session_id": session_id,
|
||||
"user_id": session_data.get("user_id"),
|
||||
@@ -548,11 +574,15 @@ async def list_active_sessions(db: Session = Depends(get_db)):
|
||||
raise HTTPException(status_code=500, detail="Failed to list sessions")
|
||||
|
||||
@router.delete("/sessions/cleanup")
|
||||
async def cleanup_old_sessions(db: Session = Depends(get_db)):
|
||||
async def cleanup_old_sessions(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Clean up old sessions.
|
||||
Clean up old sessions for the authenticated user.
|
||||
"""
|
||||
try:
|
||||
clerk_user_id = str(current_user.get('id'))
|
||||
# Initialize service with database session for active strategy access
|
||||
calendar_service = CalendarGenerationService(db)
|
||||
|
||||
|
||||
@@ -38,13 +38,16 @@ router = APIRouter(prefix="/gap-analysis", tags=["gap-analysis"])
|
||||
@router.post("/", response_model=ContentGapAnalysisResponse)
|
||||
async def create_content_gap_analysis(
|
||||
analysis: ContentGapAnalysisCreate,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new content gap analysis."""
|
||||
try:
|
||||
logger.info(f"Creating content gap analysis for: {analysis.website_url}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Creating content gap analysis for: {analysis.website_url} by user: {clerk_user_id}")
|
||||
|
||||
analysis_data = analysis.dict()
|
||||
analysis_data['user_id'] = clerk_user_id
|
||||
created_analysis = await gap_analysis_service.create_gap_analysis(analysis_data, db)
|
||||
|
||||
return ContentGapAnalysisResponse(**created_analysis)
|
||||
@@ -76,11 +79,13 @@ async def get_content_gap_analyses(
|
||||
@router.get("/{analysis_id}", response_model=ContentGapAnalysisResponse)
|
||||
async def get_content_gap_analysis(
|
||||
analysis_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get a specific content gap analysis by ID."""
|
||||
try:
|
||||
logger.info(f"Fetching content gap analysis: {analysis_id}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Fetching content gap analysis: {analysis_id} for user: {clerk_user_id}")
|
||||
|
||||
analysis = await gap_analysis_service.get_gap_analysis_by_id(analysis_id, db)
|
||||
return ContentGapAnalysisResponse(**analysis)
|
||||
@@ -117,15 +122,17 @@ async def analyze_content_gaps(
|
||||
@router.get("/user/{user_id}/analyses")
|
||||
async def get_user_gap_analyses(
|
||||
user_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all gap analyses for a specific user."""
|
||||
"""Get all gap analyses for the authenticated user."""
|
||||
try:
|
||||
logger.info(f"Fetching gap analyses for user: {user_id}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Fetching gap analyses for authenticated user: {clerk_user_id}")
|
||||
|
||||
analyses = await gap_analysis_service.get_user_gap_analyses(user_id, db)
|
||||
analyses = await gap_analysis_service.get_user_gap_analyses(clerk_user_id, db)
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"user_id": clerk_user_id,
|
||||
"analyses": analyses,
|
||||
"total_count": len(analyses)
|
||||
}
|
||||
@@ -138,11 +145,13 @@ async def get_user_gap_analyses(
|
||||
async def update_content_gap_analysis(
|
||||
analysis_id: int,
|
||||
update_data: Dict[str, Any],
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update a content gap analysis."""
|
||||
try:
|
||||
logger.info(f"Updating content gap analysis: {analysis_id}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Updating content gap analysis: {analysis_id} for user: {clerk_user_id}")
|
||||
|
||||
updated_analysis = await gap_analysis_service.update_gap_analysis(analysis_id, update_data, db)
|
||||
return ContentGapAnalysisResponse(**updated_analysis)
|
||||
@@ -156,11 +165,13 @@ async def update_content_gap_analysis(
|
||||
@router.delete("/{analysis_id}")
|
||||
async def delete_content_gap_analysis(
|
||||
analysis_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Delete a content gap analysis."""
|
||||
try:
|
||||
logger.info(f"Deleting content gap analysis: {analysis_id}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Deleting content gap analysis: {analysis_id} for user: {clerk_user_id}")
|
||||
|
||||
deleted = await gap_analysis_service.delete_gap_analysis(analysis_id, db)
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
|
||||
# Import authentication
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
# Import database service
|
||||
from services.database import get_db_session, get_db
|
||||
from services.content_planning_db import ContentPlanningDBService
|
||||
@@ -28,7 +31,9 @@ ai_analysis_db_service = AIAnalysisDBService()
|
||||
router = APIRouter(prefix="/health", tags=["health-monitoring"])
|
||||
|
||||
@router.get("/backend", response_model=Dict[str, Any])
|
||||
async def check_backend_health():
|
||||
async def check_backend_health(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Check core backend health (independent of AI services)
|
||||
"""
|
||||
@@ -77,7 +82,9 @@ async def check_backend_health():
|
||||
}
|
||||
|
||||
@router.get("/ai", response_model=Dict[str, Any])
|
||||
async def check_ai_services_health():
|
||||
async def check_ai_services_health(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Check AI services health separately
|
||||
"""
|
||||
@@ -136,7 +143,10 @@ async def check_ai_services_health():
|
||||
}
|
||||
|
||||
@router.get("/database", response_model=Dict[str, Any])
|
||||
async def database_health_check(db: Session = Depends(get_db)):
|
||||
async def database_health_check(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Health check for database operations.
|
||||
"""
|
||||
@@ -157,7 +167,10 @@ async def database_health_check(db: Session = Depends(get_db)):
|
||||
)
|
||||
|
||||
@router.get("/debug/strategies/{user_id}")
|
||||
async def debug_content_strategies(user_id: int):
|
||||
async def debug_content_strategies(
|
||||
user_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Debug endpoint to print content strategy data directly.
|
||||
"""
|
||||
@@ -203,7 +216,9 @@ async def debug_content_strategies(user_id: int):
|
||||
)
|
||||
|
||||
@router.get("/comprehensive", response_model=Dict[str, Any])
|
||||
async def comprehensive_health_check():
|
||||
async def comprehensive_health_check(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Comprehensive health check for all content planning services.
|
||||
"""
|
||||
|
||||
@@ -93,7 +93,10 @@ async def get_lightweight_statistics(current_user: Dict[str, Any] = Depends(get_
|
||||
}
|
||||
|
||||
@router.get("/cache-stats")
|
||||
async def get_cache_statistics(db = None) -> Dict[str, Any]:
|
||||
async def get_cache_statistics(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Get comprehensive user data cache statistics."""
|
||||
try:
|
||||
if not db:
|
||||
|
||||
@@ -35,15 +35,18 @@ router = APIRouter(prefix="/strategies", tags=["strategies"])
|
||||
@router.post("/", response_model=ContentStrategyResponse)
|
||||
async def create_content_strategy(
|
||||
strategy: ContentStrategyCreate,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new content strategy."""
|
||||
try:
|
||||
logger.info(f"Creating content strategy: {strategy.name}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Creating content strategy: {strategy.name} for user: {clerk_user_id}")
|
||||
|
||||
db_service = EnhancedStrategyDBService(db)
|
||||
strategy_service = EnhancedStrategyService(db_service)
|
||||
strategy_data = strategy.dict()
|
||||
strategy_data['user_id'] = clerk_user_id
|
||||
created_strategy = await strategy_service.create_enhanced_strategy(strategy_data, db)
|
||||
|
||||
return ContentStrategyResponse(**created_strategy)
|
||||
@@ -105,11 +108,13 @@ async def get_content_strategies(
|
||||
@router.get("/{strategy_id}", response_model=ContentStrategyResponse)
|
||||
async def get_content_strategy(
|
||||
strategy_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get a specific content strategy by ID."""
|
||||
try:
|
||||
logger.info(f"Fetching content strategy: {strategy_id}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Fetching content strategy: {strategy_id} for user: {clerk_user_id}")
|
||||
|
||||
db_service = EnhancedStrategyDBService(db)
|
||||
strategy_service = EnhancedStrategyService(db_service)
|
||||
@@ -127,11 +132,13 @@ async def get_content_strategy(
|
||||
async def update_content_strategy(
|
||||
strategy_id: int,
|
||||
update_data: Dict[str, Any],
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update a content strategy."""
|
||||
try:
|
||||
logger.info(f"Updating content strategy: {strategy_id}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Updating content strategy: {strategy_id} for user: {clerk_user_id}")
|
||||
|
||||
db_service = EnhancedStrategyDBService(db)
|
||||
updated_strategy = await db_service.update_enhanced_strategy(strategy_id, update_data)
|
||||
@@ -150,11 +157,13 @@ async def update_content_strategy(
|
||||
@router.delete("/{strategy_id}")
|
||||
async def delete_content_strategy(
|
||||
strategy_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Delete a content strategy."""
|
||||
try:
|
||||
logger.info(f"Deleting content strategy: {strategy_id}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Deleting content strategy: {strategy_id} for user: {clerk_user_id}")
|
||||
|
||||
db_service = EnhancedStrategyDBService(db)
|
||||
deleted = await db_service.delete_enhanced_strategy(strategy_id)
|
||||
@@ -173,11 +182,13 @@ async def delete_content_strategy(
|
||||
@router.get("/{strategy_id}/analytics")
|
||||
async def get_strategy_analytics(
|
||||
strategy_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get analytics for a specific strategy."""
|
||||
try:
|
||||
logger.info(f"Fetching analytics for strategy: {strategy_id}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Fetching analytics for strategy: {strategy_id} for user: {clerk_user_id}")
|
||||
|
||||
db_service = EnhancedStrategyDBService(db)
|
||||
analytics = await db_service.get_enhanced_strategies_with_analytics(strategy_id)
|
||||
@@ -194,11 +205,13 @@ async def get_strategy_analytics(
|
||||
@router.get("/{strategy_id}/summary")
|
||||
async def get_strategy_summary(
|
||||
strategy_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get a comprehensive summary of a strategy with analytics."""
|
||||
try:
|
||||
logger.info(f"Fetching summary for strategy: {strategy_id}")
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
logger.info(f"Fetching summary for strategy: {strategy_id} for user: {clerk_user_id}")
|
||||
|
||||
# Get strategy with analytics for comprehensive summary
|
||||
db_service = EnhancedStrategyDBService(db)
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
"""
|
||||
Quality Validation Service
|
||||
AI response quality assessment and strategic analysis.
|
||||
All methods derive results from actual input data — no hardcoded defaults.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, List
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class QualityValidationService:
|
||||
"""Service for quality validation and strategic analysis."""
|
||||
|
||||
|
||||
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.
|
||||
@@ -54,7 +55,10 @@ class QualityValidationService:
|
||||
_check(data, schema)
|
||||
|
||||
def calculate_strategic_scores(self, ai_recommendations: Dict[str, Any]) -> Dict[str, float]:
|
||||
"""Calculate strategic performance scores from AI recommendations."""
|
||||
"""Calculate strategic performance scores from AI recommendations.
|
||||
Scores are derived per analysis type from actual metrics, then aggregated
|
||||
with dimension-specific weightings — no blanket multipliers.
|
||||
"""
|
||||
scores = {
|
||||
'overall_score': 0.0,
|
||||
'content_quality_score': 0.0,
|
||||
@@ -62,87 +66,214 @@ class QualityValidationService:
|
||||
'conversion_score': 0.0,
|
||||
'innovation_score': 0.0
|
||||
}
|
||||
|
||||
# Calculate scores based on AI recommendations
|
||||
total_confidence = 0
|
||||
total_score = 0
|
||||
|
||||
for analysis_type, recommendations in ai_recommendations.items():
|
||||
if isinstance(recommendations, dict) and 'metrics' in recommendations:
|
||||
metrics = recommendations['metrics']
|
||||
score = metrics.get('score', 50)
|
||||
confidence = metrics.get('confidence', 0.5)
|
||||
|
||||
total_score += score * confidence
|
||||
total_confidence += confidence
|
||||
|
||||
if total_confidence > 0:
|
||||
scores['overall_score'] = total_score / total_confidence
|
||||
|
||||
# Set other scores based on overall score
|
||||
scores['content_quality_score'] = scores['overall_score'] * 1.1
|
||||
scores['engagement_score'] = scores['overall_score'] * 0.9
|
||||
scores['conversion_score'] = scores['overall_score'] * 0.95
|
||||
scores['innovation_score'] = scores['overall_score'] * 1.05
|
||||
|
||||
return scores
|
||||
|
||||
def extract_market_positioning(self, ai_recommendations: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Extract market positioning from AI recommendations."""
|
||||
return {
|
||||
'industry_position': 'emerging',
|
||||
'competitive_advantage': 'AI-powered content',
|
||||
'market_share': '2.5%',
|
||||
'positioning_score': 4
|
||||
|
||||
analysis_count = 0
|
||||
weighted_total = 0.0
|
||||
weight_sum = 0.0
|
||||
|
||||
# Dimension-specific weights
|
||||
dimension_weights = {
|
||||
'comprehensive_strategy': {'quality': 0.35, 'engagement': 0.20, 'conversion': 0.25, 'innovation': 0.20},
|
||||
'audience_intelligence': {'quality': 0.25, 'engagement': 0.40, 'conversion': 0.20, 'innovation': 0.15},
|
||||
'competitive_intelligence': {'quality': 0.30, 'engagement': 0.15, 'conversion': 0.25, 'innovation': 0.30},
|
||||
'performance_optimization': {'quality': 0.20, 'engagement': 0.15, 'conversion': 0.45, 'innovation': 0.20},
|
||||
'content_calendar_optimization': {'quality': 0.30, 'engagement': 0.25, 'conversion': 0.20, 'innovation': 0.25},
|
||||
}
|
||||
|
||||
|
||||
for analysis_type, recommendations in ai_recommendations.items():
|
||||
if not isinstance(recommendations, dict):
|
||||
continue
|
||||
metrics = recommendations.get('metrics')
|
||||
if not isinstance(metrics, dict):
|
||||
continue
|
||||
|
||||
score = metrics.get('score', 50)
|
||||
confidence = metrics.get('confidence', 0.5)
|
||||
weight = confidence
|
||||
|
||||
weighted_total += score * weight
|
||||
weight_sum += weight
|
||||
analysis_count += 1
|
||||
|
||||
weights = dimension_weights.get(analysis_type, {'quality': 0.25, 'engagement': 0.25, 'conversion': 0.25, 'innovation': 0.25})
|
||||
scores['content_quality_score'] += (score * weights['quality'] * weight)
|
||||
scores['engagement_score'] += (score * weights['engagement'] * weight)
|
||||
scores['conversion_score'] += (score * weights['conversion'] * weight)
|
||||
scores['innovation_score'] += (score * weights['innovation'] * weight)
|
||||
|
||||
if weight_sum > 0:
|
||||
scores['overall_score'] = round(weighted_total / weight_sum, 2)
|
||||
scores['content_quality_score'] = round(scores['content_quality_score'] / weight_sum, 2)
|
||||
scores['engagement_score'] = round(scores['engagement_score'] / weight_sum, 2)
|
||||
scores['conversion_score'] = round(scores['conversion_score'] / weight_sum, 2)
|
||||
scores['innovation_score'] = round(scores['innovation_score'] / weight_sum, 2)
|
||||
|
||||
return scores
|
||||
|
||||
def extract_market_positioning(self, ai_recommendations: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Extract market positioning from AI recommendations.
|
||||
Scans all analysis types for positioning, competitive_advantage, and market_share signals.
|
||||
Returns empty dict if no data is available instead of synthetic defaults.
|
||||
"""
|
||||
positioning = {}
|
||||
best_confidence = 0.0
|
||||
|
||||
for analysis_type, recommendations in ai_recommendations.items():
|
||||
if not isinstance(recommendations, dict):
|
||||
continue
|
||||
metrics = recommendations.get('metrics', {})
|
||||
confidence = metrics.get('confidence', 0.0)
|
||||
if confidence <= best_confidence:
|
||||
continue
|
||||
|
||||
recs = recommendations.get('recommendations', [])
|
||||
if isinstance(recs, list):
|
||||
for r in recs:
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
pos = r.get('market_position') or r.get('positioning')
|
||||
adv = r.get('competitive_advantage')
|
||||
share = r.get('market_share')
|
||||
score = r.get('positioning_score') or metrics.get('positioning_score')
|
||||
if any([pos, adv, share, score]):
|
||||
best_confidence = confidence
|
||||
if pos:
|
||||
positioning['industry_position'] = pos
|
||||
if adv:
|
||||
positioning['competitive_advantage'] = adv
|
||||
if share:
|
||||
positioning['market_share'] = str(share)
|
||||
if score is not None:
|
||||
positioning['positioning_score'] = score
|
||||
|
||||
# Check top-level keys as fallback
|
||||
if not positioning:
|
||||
for key in ('industry_position', 'competitive_advantage', 'market_share', 'positioning_score'):
|
||||
val = ai_recommendations.get(key)
|
||||
if val is not None:
|
||||
positioning[key] = val
|
||||
|
||||
return positioning
|
||||
|
||||
def extract_competitive_advantages(self, ai_recommendations: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Extract competitive advantages from AI recommendations."""
|
||||
return [
|
||||
{
|
||||
'advantage': 'AI-powered content creation',
|
||||
'impact': 'High',
|
||||
'implementation': 'In Progress'
|
||||
},
|
||||
{
|
||||
'advantage': 'Data-driven strategy',
|
||||
'impact': 'Medium',
|
||||
'implementation': 'Complete'
|
||||
}
|
||||
]
|
||||
|
||||
"""Extract competitive advantages from AI recommendations.
|
||||
Scans competitive_intelligence and other analysis types for advantage signals.
|
||||
Returns empty list if no data is available.
|
||||
"""
|
||||
advantages = []
|
||||
|
||||
for analysis_type, recommendations in ai_recommendations.items():
|
||||
if not isinstance(recommendations, dict):
|
||||
continue
|
||||
recs = recommendations.get('recommendations', [])
|
||||
if not isinstance(recs, list):
|
||||
continue
|
||||
for r in recs:
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
adv = r.get('advantage') or r.get('competitive_advantage')
|
||||
if adv:
|
||||
advantages.append({
|
||||
'advantage': adv,
|
||||
'impact': r.get('impact', 'Medium'),
|
||||
'implementation': r.get('implementation', 'Planned')
|
||||
})
|
||||
|
||||
# Deduplicate by advantage text
|
||||
seen = set()
|
||||
unique = []
|
||||
for a in advantages:
|
||||
key = a['advantage'].strip().lower()
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique.append(a)
|
||||
|
||||
return unique
|
||||
|
||||
def extract_strategic_risks(self, ai_recommendations: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Extract strategic risks from AI recommendations."""
|
||||
return [
|
||||
{
|
||||
'risk': 'Content saturation in market',
|
||||
'probability': 'Medium',
|
||||
'impact': 'High'
|
||||
},
|
||||
{
|
||||
'risk': 'Algorithm changes affecting reach',
|
||||
'probability': 'High',
|
||||
'impact': 'Medium'
|
||||
}
|
||||
]
|
||||
|
||||
"""Extract strategic risks from AI recommendations.
|
||||
Scans all analysis types for risk signals.
|
||||
Returns empty list if no data is available.
|
||||
"""
|
||||
risks = []
|
||||
|
||||
for analysis_type, recommendations in ai_recommendations.items():
|
||||
if not isinstance(recommendations, dict):
|
||||
continue
|
||||
recs = recommendations.get('recommendations', [])
|
||||
if not isinstance(recs, list):
|
||||
continue
|
||||
for r in recs:
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
risk_text = r.get('risk') or r.get('strategic_risk') or r.get('threat')
|
||||
if risk_text:
|
||||
risks.append({
|
||||
'risk': risk_text,
|
||||
'probability': r.get('probability', 'Medium'),
|
||||
'impact': r.get('impact', 'Medium')
|
||||
})
|
||||
|
||||
risks_list = recommendations.get('risks') or recommendations.get('strategic_risks')
|
||||
if isinstance(risks_list, list):
|
||||
for r in risks_list:
|
||||
if isinstance(r, dict) and r.get('risk'):
|
||||
risks.append(r)
|
||||
|
||||
seen = set()
|
||||
unique = []
|
||||
for r in risks:
|
||||
key = r['risk'].strip().lower()
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique.append(r)
|
||||
|
||||
return unique
|
||||
|
||||
def extract_opportunity_analysis(self, ai_recommendations: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Extract opportunity analysis from AI recommendations."""
|
||||
return [
|
||||
{
|
||||
'opportunity': 'Video content expansion',
|
||||
'potential_impact': 'High',
|
||||
'implementation_ease': 'Medium'
|
||||
},
|
||||
{
|
||||
'opportunity': 'Social media engagement',
|
||||
'potential_impact': 'Medium',
|
||||
'implementation_ease': 'High'
|
||||
}
|
||||
]
|
||||
|
||||
"""Extract opportunity analysis from AI recommendations.
|
||||
Scans all analysis types for opportunity signals.
|
||||
Returns empty list if no data is available.
|
||||
"""
|
||||
opportunities = []
|
||||
|
||||
for analysis_type, recommendations in ai_recommendations.items():
|
||||
if not isinstance(recommendations, dict):
|
||||
continue
|
||||
recs = recommendations.get('recommendations', [])
|
||||
if not isinstance(recs, list):
|
||||
continue
|
||||
for r in recs:
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
opp = r.get('opportunity') or r.get('growth_opportunity')
|
||||
if opp:
|
||||
opportunities.append({
|
||||
'opportunity': opp,
|
||||
'potential_impact': r.get('potential_impact', 'Medium'),
|
||||
'implementation_ease': r.get('implementation_ease', 'Medium')
|
||||
})
|
||||
|
||||
opps_list = recommendations.get('opportunities') or recommendations.get('growth_opportunities')
|
||||
if isinstance(opps_list, list):
|
||||
for o in opps_list:
|
||||
if isinstance(o, dict) and o.get('opportunity'):
|
||||
opportunities.append(o)
|
||||
|
||||
seen = set()
|
||||
unique = []
|
||||
for o in opportunities:
|
||||
key = o['opportunity'].strip().lower()
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique.append(o)
|
||||
|
||||
return unique
|
||||
|
||||
def validate_ai_response_quality(self, ai_response: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Validate the quality of AI response."""
|
||||
"""Validate the quality of AI response using multi-dimensional analysis.
|
||||
Scores are derived from actual content, not placeholders.
|
||||
"""
|
||||
quality_metrics = {
|
||||
'completeness': 0.0,
|
||||
'relevance': 0.0,
|
||||
@@ -150,30 +281,76 @@ class QualityValidationService:
|
||||
'confidence': 0.0,
|
||||
'overall_quality': 0.0
|
||||
}
|
||||
|
||||
# Calculate completeness
|
||||
required_fields = ['recommendations', 'insights', 'metrics']
|
||||
present_fields = sum(1 for field in required_fields if field in ai_response)
|
||||
quality_metrics['completeness'] = present_fields / len(required_fields)
|
||||
|
||||
# Calculate relevance (placeholder logic)
|
||||
quality_metrics['relevance'] = 0.8 if ai_response.get('analysis_type') else 0.5
|
||||
|
||||
# Calculate actionability (placeholder logic)
|
||||
|
||||
# Completeness: weighted by field importance
|
||||
field_weights = {
|
||||
'recommendations': 0.35,
|
||||
'insights': 0.30,
|
||||
'metrics': 0.20,
|
||||
'analysis_type': 0.15
|
||||
}
|
||||
weighted_present = 0.0
|
||||
total_weight = 0.0
|
||||
for field, weight in field_weights.items():
|
||||
total_weight += weight
|
||||
val = ai_response.get(field)
|
||||
if field == 'recommendations':
|
||||
if isinstance(val, list) and len(val) > 0:
|
||||
weighted_present += weight
|
||||
elif field == 'insights':
|
||||
if isinstance(val, list) and len(val) > 0:
|
||||
weighted_present += weight
|
||||
elif field == 'metrics':
|
||||
if isinstance(val, dict) and len(val) > 0:
|
||||
weighted_present += weight
|
||||
else:
|
||||
if val is not None:
|
||||
weighted_present += weight
|
||||
quality_metrics['completeness'] = round(weighted_present / total_weight, 2) if total_weight > 0 else 0.0
|
||||
|
||||
# Relevance: evaluate recommendations content quality
|
||||
recommendations = ai_response.get('recommendations', [])
|
||||
quality_metrics['actionability'] = min(1.0, len(recommendations) / 5.0)
|
||||
|
||||
# Calculate confidence
|
||||
if isinstance(recommendations, list) and len(recommendations) > 0:
|
||||
scored = 0
|
||||
total_recs = len(recommendations)
|
||||
for r in recommendations:
|
||||
if isinstance(r, dict):
|
||||
has_action = bool(r.get('action') or r.get('recommendation') or r.get('step'))
|
||||
has_reason = bool(r.get('reason') or r.get('rationale') or r.get('impact'))
|
||||
if has_action and has_reason:
|
||||
scored += 1
|
||||
quality_metrics['relevance'] = round(scored / total_recs, 2) if total_recs > 0 else 0.5
|
||||
else:
|
||||
quality_metrics['relevance'] = 0.0
|
||||
|
||||
# Actionability: recommendation detail score
|
||||
if isinstance(recommendations, list) and len(recommendations) > 0:
|
||||
actionable = 0
|
||||
for r in recommendations:
|
||||
if isinstance(r, dict):
|
||||
has_timeline = bool(r.get('timeline') or r.get('effort'))
|
||||
has_impact = bool(r.get('impact') or r.get('expected_outcome'))
|
||||
if has_timeline or has_impact:
|
||||
actionable += 1
|
||||
quality_metrics['actionability'] = round(min(1.0, actionable / max(len(recommendations), 1)), 2)
|
||||
else:
|
||||
quality_metrics['actionability'] = 0.0
|
||||
|
||||
# Confidence from metrics
|
||||
metrics = ai_response.get('metrics', {})
|
||||
quality_metrics['confidence'] = metrics.get('confidence', 0.5)
|
||||
|
||||
# Calculate overall quality
|
||||
quality_metrics['overall_quality'] = sum(quality_metrics.values()) / len(quality_metrics)
|
||||
|
||||
quality_metrics['confidence'] = round(metrics.get('confidence', 0.0), 2) if isinstance(metrics, dict) else 0.0
|
||||
|
||||
# Overall weighted quality
|
||||
weights = {'completeness': 0.25, 'relevance': 0.30, 'actionability': 0.25, 'confidence': 0.20}
|
||||
overall = sum(quality_metrics[k] * weights[k] for k in weights)
|
||||
quality_metrics['overall_quality'] = round(overall, 2)
|
||||
|
||||
return quality_metrics
|
||||
|
||||
|
||||
def assess_strategy_quality(self, strategy_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Assess the overall quality of a content strategy."""
|
||||
"""Assess the overall quality of a content strategy.
|
||||
Uses field-level analysis with content-aware scoring — not simple presence checks.
|
||||
"""
|
||||
quality_assessment = {
|
||||
'data_completeness': 0.0,
|
||||
'strategic_clarity': 0.0,
|
||||
@@ -181,25 +358,59 @@ class QualityValidationService:
|
||||
'competitive_positioning': 0.0,
|
||||
'overall_quality': 0.0
|
||||
}
|
||||
|
||||
# Assess data completeness
|
||||
required_fields = [
|
||||
'business_objectives', 'target_metrics', 'content_budget',
|
||||
'team_size', 'implementation_timeline'
|
||||
]
|
||||
present_fields = sum(1 for field in required_fields if strategy_data.get(field))
|
||||
quality_assessment['data_completeness'] = present_fields / len(required_fields)
|
||||
|
||||
# Assess strategic clarity (placeholder logic)
|
||||
quality_assessment['strategic_clarity'] = 0.7 if strategy_data.get('business_objectives') else 0.3
|
||||
|
||||
# Assess implementation readiness (placeholder logic)
|
||||
quality_assessment['implementation_readiness'] = 0.6 if strategy_data.get('team_size') else 0.2
|
||||
|
||||
# Assess competitive positioning (placeholder logic)
|
||||
quality_assessment['competitive_positioning'] = 0.5 if strategy_data.get('competitive_position') else 0.2
|
||||
|
||||
# Calculate overall quality
|
||||
quality_assessment['overall_quality'] = sum(quality_assessment.values()) / len(quality_assessment)
|
||||
|
||||
|
||||
# Data completeness with weighted field groups
|
||||
field_groups = {
|
||||
'objectives': {'fields': ['business_objectives', 'target_metrics'], 'weight': 0.25},
|
||||
'resources': {'fields': ['content_budget', 'team_size', 'implementation_timeline'], 'weight': 0.25},
|
||||
'audience': {'fields': ['content_preferences', 'consumption_patterns', 'audience_pain_points'], 'weight': 0.25},
|
||||
'competition': {'fields': ['top_competitors', 'market_gaps', 'competitive_position'], 'weight': 0.25}
|
||||
}
|
||||
total_weight = 0.0
|
||||
weighted_score = 0.0
|
||||
for group_name, group in field_groups.items():
|
||||
group_present = sum(1 for f in group['fields'] if strategy_data.get(f) not in (None, '', []))
|
||||
group_score = group_present / len(group['fields']) if group['fields'] else 0
|
||||
weighted_score += group_score * group['weight']
|
||||
total_weight += group['weight']
|
||||
quality_assessment['data_completeness'] = round(weighted_score / total_weight, 2) if total_weight > 0 else 0.0
|
||||
|
||||
# Strategic clarity: evaluate quality of business objectives
|
||||
objectives = strategy_data.get('business_objectives')
|
||||
if isinstance(objectives, str) and len(objectives) > 20:
|
||||
quality_assessment['strategic_clarity'] = 0.9
|
||||
elif isinstance(objectives, str) and len(objectives) > 0:
|
||||
quality_assessment['strategic_clarity'] = 0.6
|
||||
elif isinstance(objectives, list) and len(objectives) > 0:
|
||||
quality_assessment['strategic_clarity'] = 0.8
|
||||
else:
|
||||
quality_assessment['strategic_clarity'] = 0.0
|
||||
|
||||
# Implementation readiness: budget + team + timeline
|
||||
readiness_signals = 0
|
||||
if strategy_data.get('content_budget') not in (None, '', 0):
|
||||
readiness_signals += 1
|
||||
if strategy_data.get('team_size') not in (None, '', 0):
|
||||
readiness_signals += 1
|
||||
if strategy_data.get('implementation_timeline') not in (None, '', []):
|
||||
readiness_signals += 1
|
||||
quality_assessment['implementation_readiness'] = round(readiness_signals / 3.0, 2)
|
||||
|
||||
# Competitive positioning: evaluate depth of competitive data
|
||||
comp_signals = 0
|
||||
if strategy_data.get('top_competitors') not in (None, '', []):
|
||||
comp_signals += 1
|
||||
if strategy_data.get('market_gaps') not in (None, '', []):
|
||||
comp_signals += 1
|
||||
if strategy_data.get('competitive_position') not in (None, ''):
|
||||
comp_signals += 1
|
||||
if strategy_data.get('industry_trends') not in (None, '', []):
|
||||
comp_signals += 1
|
||||
quality_assessment['competitive_positioning'] = round(comp_signals / 4.0, 2)
|
||||
|
||||
# Overall quality
|
||||
quality_assessment['overall_quality'] = round(
|
||||
sum(quality_assessment.values()) / len(quality_assessment), 2
|
||||
)
|
||||
|
||||
return quality_assessment
|
||||
@@ -510,7 +510,7 @@ class EnhancedStrategyService:
|
||||
async def get_system_health(self, db: Session) -> Dict[str, Any]:
|
||||
"""Get system health status."""
|
||||
try:
|
||||
return await self.health_monitoring_service.get_system_health(db)
|
||||
return await self.health_monitoring_service.check_system_health(db)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting system health: {str(e)}")
|
||||
raise
|
||||
@@ -583,7 +583,7 @@ class EnhancedStrategyService:
|
||||
async def optimize_strategy_operation(self, operation_name: str, operation_func, *args, **kwargs) -> Dict[str, Any]:
|
||||
"""Optimize strategy operation with performance monitoring."""
|
||||
try:
|
||||
return await self.performance_optimization_service.optimize_operation(
|
||||
return await self.performance_optimization_service.optimize_response_time(
|
||||
operation_name, operation_func, *args, **kwargs
|
||||
)
|
||||
except Exception as e:
|
||||
|
||||
@@ -176,11 +176,7 @@ class FieldTransformationService:
|
||||
# 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:
|
||||
if field_value is not None and field_value != "":
|
||||
transformed_fields[field_name] = {
|
||||
'value': field_value,
|
||||
'source': sources[0] if sources else 'default',
|
||||
@@ -943,44 +939,6 @@ class FieldTransformationService:
|
||||
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:
|
||||
|
||||
@@ -44,6 +44,11 @@ class CachingService:
|
||||
'ttl': 900, # 15 minutes
|
||||
'max_size': 1000,
|
||||
'priority': 'low'
|
||||
},
|
||||
'streaming_intelligence': {
|
||||
'ttl': 300, # 5 minutes
|
||||
'max_size': 500,
|
||||
'priority': 'medium'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from .data_processors import (
|
||||
transform_onboarding_data_to_fields,
|
||||
get_data_sources,
|
||||
get_detailed_input_data_points,
|
||||
get_fallback_onboarding_data,
|
||||
get_website_analysis_data,
|
||||
get_research_preferences_data,
|
||||
get_api_keys_data
|
||||
@@ -36,7 +35,6 @@ __all__ = [
|
||||
'transform_onboarding_data_to_fields',
|
||||
'get_data_sources',
|
||||
'get_detailed_input_data_points',
|
||||
'get_fallback_onboarding_data',
|
||||
'get_website_analysis_data',
|
||||
'get_research_preferences_data',
|
||||
'get_api_keys_data',
|
||||
|
||||
@@ -179,17 +179,13 @@ class DataProcessorService:
|
||||
}
|
||||
|
||||
fields['seasonal_trends'] = {
|
||||
'value': ['Q1: Planning', 'Q2: Execution', 'Q3: Optimization', 'Q4: Review'],
|
||||
'value': research_data.get('seasonal_trends', []),
|
||||
'source': 'research_preferences',
|
||||
'confidence': research_data.get('confidence_level', 0.7)
|
||||
}
|
||||
|
||||
fields['engagement_metrics'] = {
|
||||
'value': {
|
||||
'avg_session_duration': website_data.get('performance_metrics', {}).get('avg_session_duration', 180),
|
||||
'bounce_rate': website_data.get('performance_metrics', {}).get('bounce_rate', 45.5),
|
||||
'pages_per_session': 2.5
|
||||
},
|
||||
'value': website_data.get('performance_metrics', {}),
|
||||
'source': 'website_analysis',
|
||||
'confidence': website_data.get('confidence_level', 0.8)
|
||||
}
|
||||
@@ -411,15 +407,6 @@ class DataProcessorService:
|
||||
}
|
||||
}
|
||||
|
||||
def get_fallback_onboarding_data(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get fallback onboarding data for compatibility.
|
||||
|
||||
Returns:
|
||||
Dictionary with fallback data (raises error as fallbacks are disabled)
|
||||
"""
|
||||
raise RuntimeError("Fallback onboarding data is disabled. Real data required.")
|
||||
|
||||
async def get_website_analysis_data(self, user_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Get website analysis data from onboarding.
|
||||
@@ -534,12 +521,6 @@ def get_detailed_input_data_points(processed_data: Dict[str, Any]) -> Dict[str,
|
||||
return processor.get_detailed_input_data_points(processed_data)
|
||||
|
||||
|
||||
def get_fallback_onboarding_data() -> Dict[str, Any]:
|
||||
"""Get fallback onboarding data for compatibility."""
|
||||
processor = DataProcessorService()
|
||||
return processor.get_fallback_onboarding_data()
|
||||
|
||||
|
||||
async def get_website_analysis_data(user_id: int) -> Dict[str, Any]:
|
||||
"""Get website analysis data from onboarding."""
|
||||
processor = DataProcessorService()
|
||||
|
||||
@@ -14,6 +14,7 @@ logger = logging.getLogger(__name__)
|
||||
def calculate_strategic_scores(ai_recommendations: Dict[str, Any]) -> Dict[str, float]:
|
||||
"""
|
||||
Calculate strategic performance scores from AI recommendations.
|
||||
Dimension-specific weights — no blanket multipliers.
|
||||
|
||||
Args:
|
||||
ai_recommendations: Dictionary containing AI analysis results
|
||||
@@ -28,35 +29,48 @@ def calculate_strategic_scores(ai_recommendations: Dict[str, Any]) -> Dict[str,
|
||||
'conversion_score': 0.0,
|
||||
'innovation_score': 0.0
|
||||
}
|
||||
|
||||
# Calculate scores based on AI recommendations
|
||||
total_confidence = 0
|
||||
total_score = 0
|
||||
|
||||
|
||||
weight_sum = 0.0
|
||||
|
||||
dimension_weights = {
|
||||
'comprehensive_strategy': {'quality': 0.35, 'engagement': 0.20, 'conversion': 0.25, 'innovation': 0.20},
|
||||
'audience_intelligence': {'quality': 0.25, 'engagement': 0.40, 'conversion': 0.20, 'innovation': 0.15},
|
||||
'competitive_intelligence': {'quality': 0.30, 'engagement': 0.15, 'conversion': 0.25, 'innovation': 0.30},
|
||||
'performance_optimization': {'quality': 0.20, 'engagement': 0.15, 'conversion': 0.45, 'innovation': 0.20},
|
||||
'content_calendar_optimization': {'quality': 0.30, 'engagement': 0.25, 'conversion': 0.20, 'innovation': 0.25},
|
||||
}
|
||||
|
||||
for analysis_type, recommendations in ai_recommendations.items():
|
||||
if isinstance(recommendations, dict) and 'metrics' in recommendations:
|
||||
metrics = recommendations['metrics']
|
||||
score = metrics.get('score', 50)
|
||||
confidence = metrics.get('confidence', 0.5)
|
||||
|
||||
total_score += score * confidence
|
||||
total_confidence += confidence
|
||||
|
||||
if total_confidence > 0:
|
||||
scores['overall_score'] = total_score / total_confidence
|
||||
|
||||
# Set other scores based on overall score
|
||||
scores['content_quality_score'] = scores['overall_score'] * 1.1
|
||||
scores['engagement_score'] = scores['overall_score'] * 0.9
|
||||
scores['conversion_score'] = scores['overall_score'] * 0.95
|
||||
scores['innovation_score'] = scores['overall_score'] * 1.05
|
||||
|
||||
if not isinstance(recommendations, dict):
|
||||
continue
|
||||
metrics = recommendations.get('metrics')
|
||||
if not isinstance(metrics, dict):
|
||||
continue
|
||||
|
||||
score = metrics.get('score', 50)
|
||||
confidence = metrics.get('confidence', 0.5)
|
||||
weight = confidence
|
||||
|
||||
scores['overall_score'] += score * weight
|
||||
weight_sum += weight
|
||||
|
||||
weights = dimension_weights.get(analysis_type, {'quality': 0.25, 'engagement': 0.25, 'conversion': 0.25, 'innovation': 0.25})
|
||||
scores['content_quality_score'] += score * weights['quality'] * weight
|
||||
scores['engagement_score'] += score * weights['engagement'] * weight
|
||||
scores['conversion_score'] += score * weights['conversion'] * weight
|
||||
scores['innovation_score'] += score * weights['innovation'] * weight
|
||||
|
||||
if weight_sum > 0:
|
||||
for k in scores:
|
||||
scores[k] = round(scores[k] / weight_sum, 2)
|
||||
|
||||
return scores
|
||||
|
||||
|
||||
def extract_market_positioning(ai_recommendations: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract market positioning insights from AI recommendations.
|
||||
Scans all analysis types for positioning signals. Returns empty dict if none found.
|
||||
|
||||
Args:
|
||||
ai_recommendations: Dictionary containing AI analysis results
|
||||
@@ -64,17 +78,50 @@ def extract_market_positioning(ai_recommendations: Dict[str, Any]) -> Dict[str,
|
||||
Returns:
|
||||
Dictionary with market positioning data
|
||||
"""
|
||||
return {
|
||||
'industry_position': 'emerging',
|
||||
'competitive_advantage': 'AI-powered content',
|
||||
'market_share': '2.5%',
|
||||
'positioning_score': 4
|
||||
}
|
||||
positioning = {}
|
||||
best_confidence = 0.0
|
||||
|
||||
for analysis_type, recommendations in ai_recommendations.items():
|
||||
if not isinstance(recommendations, dict):
|
||||
continue
|
||||
metrics = recommendations.get('metrics', {})
|
||||
confidence = metrics.get('confidence', 0.0)
|
||||
if confidence <= best_confidence:
|
||||
continue
|
||||
|
||||
recs = recommendations.get('recommendations', [])
|
||||
if isinstance(recs, list):
|
||||
for r in recs:
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
pos = r.get('market_position') or r.get('positioning')
|
||||
adv = r.get('competitive_advantage')
|
||||
share = r.get('market_share')
|
||||
score = r.get('positioning_score') or metrics.get('positioning_score')
|
||||
if any([pos, adv, share, score]):
|
||||
best_confidence = confidence
|
||||
if pos:
|
||||
positioning['industry_position'] = pos
|
||||
if adv:
|
||||
positioning['competitive_advantage'] = adv
|
||||
if share:
|
||||
positioning['market_share'] = str(share)
|
||||
if score is not None:
|
||||
positioning['positioning_score'] = score
|
||||
|
||||
if not positioning:
|
||||
for key in ('industry_position', 'competitive_advantage', 'market_share', 'positioning_score'):
|
||||
val = ai_recommendations.get(key)
|
||||
if val is not None:
|
||||
positioning[key] = val
|
||||
|
||||
return positioning
|
||||
|
||||
|
||||
def extract_competitive_advantages(ai_recommendations: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Extract competitive advantages from AI recommendations.
|
||||
Scans all analysis types for advantage signals. Returns empty list if none found.
|
||||
|
||||
Args:
|
||||
ai_recommendations: Dictionary containing AI analysis results
|
||||
@@ -82,23 +129,40 @@ def extract_competitive_advantages(ai_recommendations: Dict[str, Any]) -> List[D
|
||||
Returns:
|
||||
List of competitive advantages with impact and implementation status
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'advantage': 'AI-powered content creation',
|
||||
'impact': 'High',
|
||||
'implementation': 'In Progress'
|
||||
},
|
||||
{
|
||||
'advantage': 'Data-driven strategy',
|
||||
'impact': 'Medium',
|
||||
'implementation': 'Complete'
|
||||
}
|
||||
]
|
||||
advantages = []
|
||||
|
||||
for analysis_type, recommendations in ai_recommendations.items():
|
||||
if not isinstance(recommendations, dict):
|
||||
continue
|
||||
recs = recommendations.get('recommendations', [])
|
||||
if not isinstance(recs, list):
|
||||
continue
|
||||
for r in recs:
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
adv = r.get('advantage') or r.get('competitive_advantage')
|
||||
if adv:
|
||||
advantages.append({
|
||||
'advantage': adv,
|
||||
'impact': r.get('impact', 'Medium'),
|
||||
'implementation': r.get('implementation', 'Planned')
|
||||
})
|
||||
|
||||
seen = set()
|
||||
unique = []
|
||||
for a in advantages:
|
||||
key = a['advantage'].strip().lower()
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique.append(a)
|
||||
|
||||
return unique
|
||||
|
||||
|
||||
def extract_strategic_risks(ai_recommendations: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Extract strategic risks from AI recommendations.
|
||||
Scans all analysis types for risk signals. Returns empty list if none found.
|
||||
|
||||
Args:
|
||||
ai_recommendations: Dictionary containing AI analysis results
|
||||
@@ -106,23 +170,46 @@ def extract_strategic_risks(ai_recommendations: Dict[str, Any]) -> List[Dict[str
|
||||
Returns:
|
||||
List of strategic risks with probability and impact assessment
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'risk': 'Content saturation in market',
|
||||
'probability': 'Medium',
|
||||
'impact': 'High'
|
||||
},
|
||||
{
|
||||
'risk': 'Algorithm changes affecting reach',
|
||||
'probability': 'High',
|
||||
'impact': 'Medium'
|
||||
}
|
||||
]
|
||||
risks = []
|
||||
|
||||
for analysis_type, recommendations in ai_recommendations.items():
|
||||
if not isinstance(recommendations, dict):
|
||||
continue
|
||||
recs = recommendations.get('recommendations', [])
|
||||
if not isinstance(recs, list):
|
||||
continue
|
||||
for r in recs:
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
risk_text = r.get('risk') or r.get('strategic_risk') or r.get('threat')
|
||||
if risk_text:
|
||||
risks.append({
|
||||
'risk': risk_text,
|
||||
'probability': r.get('probability', 'Medium'),
|
||||
'impact': r.get('impact', 'Medium')
|
||||
})
|
||||
|
||||
risks_list = recommendations.get('risks') or recommendations.get('strategic_risks')
|
||||
if isinstance(risks_list, list):
|
||||
for r in risks_list:
|
||||
if isinstance(r, dict) and r.get('risk'):
|
||||
risks.append(r)
|
||||
|
||||
seen = set()
|
||||
unique = []
|
||||
for r in risks:
|
||||
key = r['risk'].strip().lower()
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique.append(r)
|
||||
|
||||
return unique
|
||||
|
||||
|
||||
def extract_opportunity_analysis(ai_recommendations: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Extract opportunity analysis from AI recommendations.
|
||||
Scans all analysis types for opportunity signals. Returns empty list if none found.
|
||||
|
||||
Args:
|
||||
ai_recommendations: Dictionary containing AI analysis results
|
||||
@@ -130,18 +217,40 @@ def extract_opportunity_analysis(ai_recommendations: Dict[str, Any]) -> List[Dic
|
||||
Returns:
|
||||
List of opportunities with potential impact and implementation ease
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'opportunity': 'Video content expansion',
|
||||
'potential_impact': 'High',
|
||||
'implementation_ease': 'Medium'
|
||||
},
|
||||
{
|
||||
'opportunity': 'Social media engagement',
|
||||
'potential_impact': 'Medium',
|
||||
'implementation_ease': 'High'
|
||||
}
|
||||
]
|
||||
opportunities = []
|
||||
|
||||
for analysis_type, recommendations in ai_recommendations.items():
|
||||
if not isinstance(recommendations, dict):
|
||||
continue
|
||||
recs = recommendations.get('recommendations', [])
|
||||
if not isinstance(recs, list):
|
||||
continue
|
||||
for r in recs:
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
opp = r.get('opportunity') or r.get('growth_opportunity')
|
||||
if opp:
|
||||
opportunities.append({
|
||||
'opportunity': opp,
|
||||
'potential_impact': r.get('potential_impact', 'Medium'),
|
||||
'implementation_ease': r.get('implementation_ease', 'Medium')
|
||||
})
|
||||
|
||||
opps_list = recommendations.get('opportunities') or recommendations.get('growth_opportunities')
|
||||
if isinstance(opps_list, list):
|
||||
for o in opps_list:
|
||||
if isinstance(o, dict) and o.get('opportunity'):
|
||||
opportunities.append(o)
|
||||
|
||||
seen = set()
|
||||
unique = []
|
||||
for o in opportunities:
|
||||
key = o['opportunity'].strip().lower()
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique.append(o)
|
||||
|
||||
return unique
|
||||
|
||||
|
||||
def initialize_caches() -> Dict[str, Any]:
|
||||
|
||||
@@ -192,10 +192,6 @@ class EnhancedStrategyService:
|
||||
"""Get detailed input data points - delegates to core service."""
|
||||
return self.core_service.data_processor_service.get_detailed_input_data_points(processed_data)
|
||||
|
||||
def _get_fallback_onboarding_data(self) -> Dict[str, Any]:
|
||||
"""Get fallback onboarding data - delegates to core service."""
|
||||
return self.core_service.data_processor_service.get_fallback_onboarding_data()
|
||||
|
||||
async def _get_website_analysis_data(self, user_id: int) -> Dict[str, Any]:
|
||||
"""Get website analysis data - delegates to core service."""
|
||||
return await self.core_service.data_processor_service.get_website_analysis_data(user_id)
|
||||
@@ -220,22 +216,6 @@ class EnhancedStrategyService:
|
||||
"""Process API keys data - delegates to core service."""
|
||||
return await self.core_service.data_processor_service.process_api_keys_data(api_data)
|
||||
|
||||
def _transform_onboarding_data_to_fields(self, processed_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
# deprecated; not used
|
||||
raise RuntimeError("Deprecated: use AutoFillService.transformer")
|
||||
|
||||
def _get_data_sources(self, processed_data: Dict[str, Any]) -> Dict[str, str]:
|
||||
# deprecated; not used
|
||||
raise RuntimeError("Deprecated: use AutoFillService.transparency")
|
||||
|
||||
def _get_detailed_input_data_points(self, processed_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
# deprecated; not used
|
||||
raise RuntimeError("Deprecated: use AutoFillService.transparency")
|
||||
|
||||
def _get_fallback_onboarding_data(self) -> Dict[str, Any]:
|
||||
"""Deprecated: fallbacks are no longer permitted. Kept for compatibility; always raises."""
|
||||
raise RuntimeError("Fallback onboarding data is disabled. Real data required.")
|
||||
|
||||
def _initialize_caches(self) -> None:
|
||||
"""Initialize caches - delegates to core service."""
|
||||
# This is now handled by the core service
|
||||
|
||||
@@ -15,6 +15,7 @@ from pydantic import BaseModel, Field
|
||||
from services.llm_providers.main_image_generation import generate_image
|
||||
from services.llm_providers.main_image_editing import edit_image
|
||||
from services.llm_providers.main_text_generation import llm_text_gen
|
||||
from services.llm_providers.tenant_provider_config import tenant_provider_config_resolver
|
||||
from services.image_generation import (
|
||||
extract_visual_data as _extract_visual_data,
|
||||
get_model_recommendation,
|
||||
@@ -45,6 +46,7 @@ class ImageGenerateRequest(BaseModel):
|
||||
guidance_scale: Optional[float] = None
|
||||
steps: Optional[int] = None
|
||||
seed: Optional[int] = None
|
||||
overlay_text: Optional[str] = None
|
||||
|
||||
|
||||
class ImageGenerateResponse(BaseModel):
|
||||
@@ -58,6 +60,16 @@ class ImageGenerateResponse(BaseModel):
|
||||
seed: Optional[int] = None
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
def get_image_config(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
) -> dict:
|
||||
user_id = str(current_user.get('id', ''))
|
||||
cfg = tenant_provider_config_resolver.resolve(modality="image", user_id=user_id)
|
||||
provider = (cfg.selected_providers or [""])[0]
|
||||
return {"provider": provider}
|
||||
|
||||
|
||||
@router.post("/generate", response_model=ImageGenerateResponse)
|
||||
def generate(
|
||||
req: ImageGenerateRequest,
|
||||
@@ -90,6 +102,7 @@ def generate(
|
||||
"guidance_scale": req.guidance_scale,
|
||||
"steps": req.steps,
|
||||
"seed": req.seed,
|
||||
"overlay_text": req.overlay_text,
|
||||
},
|
||||
user_id=user_id, # Pass user_id for validation inside generate_image
|
||||
)
|
||||
@@ -167,74 +180,7 @@ def generate(
|
||||
logger.error(f"[images.generate] Unexpected error saving image: {save_error}", exc_info=True)
|
||||
# Continue without failing the request
|
||||
|
||||
# TRACK USAGE after successful image generation
|
||||
if result:
|
||||
logger.info(f"[images.generate] ✅ Image generation successful, tracking usage for user {user_id}")
|
||||
try:
|
||||
db_track = next(get_db())
|
||||
try:
|
||||
# Get or create usage summary
|
||||
pricing = PricingService(db_track)
|
||||
current_period = pricing.get_current_billing_period(user_id) or datetime.now().strftime("%Y-%m")
|
||||
|
||||
logger.debug(f"[images.generate] Looking for usage summary: user_id={user_id}, period={current_period}")
|
||||
|
||||
summary = db_track.query(UsageSummary).filter(
|
||||
UsageSummary.user_id == user_id,
|
||||
UsageSummary.billing_period == current_period
|
||||
).first()
|
||||
|
||||
if not summary:
|
||||
logger.info(f"[images.generate] Creating new usage summary for user {user_id}, period {current_period}")
|
||||
summary = UsageSummary(
|
||||
user_id=user_id,
|
||||
billing_period=current_period
|
||||
)
|
||||
db_track.add(summary)
|
||||
db_track.flush()
|
||||
|
||||
current_calls_before = getattr(summary, "stability_calls", 0) or 0
|
||||
new_calls = current_calls_before + 1
|
||||
|
||||
limits = pricing.get_user_limits(user_id)
|
||||
plan_name = limits.get('plan_name', 'unknown') if limits else 'unknown'
|
||||
tier = limits.get('tier', 'unknown') if limits else 'unknown'
|
||||
call_limit = limits['limits'].get("stability_calls", 0) if limits else 0
|
||||
|
||||
current_image_edit_calls = getattr(summary, "image_edit_calls", 0) or 0
|
||||
image_edit_limit = limits['limits'].get("image_edit_calls", 0) if limits else 0
|
||||
|
||||
current_video_calls = getattr(summary, "video_calls", 0) or 0
|
||||
video_limit = limits['limits'].get("video_calls", 0) if limits else 0
|
||||
|
||||
current_audio_calls = getattr(summary, "audio_calls", 0) or 0
|
||||
audio_limit = limits['limits'].get("audio_calls", 0) if limits else 0
|
||||
audio_limit_display = audio_limit if (audio_limit > 0 or tier != 'enterprise') else '∞'
|
||||
|
||||
logger.debug(f"[images.generate] Usage snapshot for logging: stability_calls={current_calls_before}, total_calls={summary.total_calls or 0}")
|
||||
|
||||
# UNIFIED SUBSCRIPTION LOG - Shows before/after state in one message
|
||||
print(f"""
|
||||
[SUBSCRIPTION] Image Generation
|
||||
├─ User: {user_id}
|
||||
├─ Plan: {plan_name} ({tier})
|
||||
├─ Provider: stability
|
||||
├─ Actual Provider: {result.provider}
|
||||
├─ Model: {result.model or 'default'}
|
||||
├─ Calls: {current_calls_before} → {new_calls} / {call_limit if call_limit > 0 else '∞'}
|
||||
├─ Image Editing: {current_image_edit_calls} / {image_edit_limit if image_edit_limit > 0 else '∞'}
|
||||
├─ Videos: {current_video_calls} / {video_limit if video_limit > 0 else '∞'}
|
||||
├─ Audio: {current_audio_calls} / {audio_limit_display}
|
||||
└─ Status: ✅ Allowed & Tracked
|
||||
""")
|
||||
except Exception as track_error:
|
||||
logger.error(f"[images.generate] ❌ Error tracking usage (non-blocking): {track_error}", exc_info=True)
|
||||
db_track.rollback()
|
||||
finally:
|
||||
db_track.close()
|
||||
except Exception as usage_error:
|
||||
# Non-blocking: log error but don't fail the request
|
||||
logger.error(f"[images.generate] ❌ Failed to track usage: {usage_error}", exc_info=True)
|
||||
# Usage tracking is handled inside generate_image() facade
|
||||
|
||||
# Create response with explicit success field
|
||||
# Note: Asset saving and usage tracking are non-blocking and won't affect this response
|
||||
@@ -597,12 +543,13 @@ MODEL_SPECIFIC_GUIDANCE = {
|
||||
}
|
||||
|
||||
|
||||
# Models that can render readable text directly in generated images
|
||||
_TEXT_CAPABLE = {"flux-kontext-pro", "flux-2-flex", "glm-image"}
|
||||
|
||||
|
||||
def get_model_specific_guidance(model: Optional[str], image_type: Optional[str]) -> Dict[str, Any]:
|
||||
"""Get model-specific guidance based on model and image type."""
|
||||
if not model:
|
||||
return {}
|
||||
|
||||
model_lower = model.lower()
|
||||
model_lower = (model or "_default").lower()
|
||||
image_type_lower = (image_type or "conceptual").lower()
|
||||
|
||||
# Get model guidance (use _default for unknown models)
|
||||
@@ -619,8 +566,13 @@ def suggest_prompts(
|
||||
req: ImagePromptSuggestRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
) -> ImagePromptSuggestResponse:
|
||||
user_id = str(current_user.get('id', ''))
|
||||
try:
|
||||
provider = (req.provider or ("gemini" if (os.getenv("GPT_PROVIDER") or "").lower().startswith("gemini") else "huggingface")).lower()
|
||||
if req.provider:
|
||||
provider = req.provider.lower()
|
||||
else:
|
||||
cfg = tenant_provider_config_resolver.resolve(modality="image", user_id=user_id)
|
||||
provider = (cfg.selected_providers or ["huggingface"])[0]
|
||||
model = req.model or None
|
||||
image_type = req.image_type or "conceptual"
|
||||
|
||||
@@ -677,10 +629,20 @@ def suggest_prompts(
|
||||
"required": ["suggestions"]
|
||||
}
|
||||
|
||||
can_render_text = model and model.lower() in _TEXT_CAPABLE
|
||||
|
||||
system = (
|
||||
"You are an expert image prompt engineer for text-to-image models. "
|
||||
"Given blog section context, craft 3-5 hyper-personalized prompts optimized for the specified provider. "
|
||||
"Return STRICT JSON matching the provided schema, no extra text."
|
||||
"You are an expert image prompt engineer. "
|
||||
"Given blog section context, craft 3-5 concise prompts optimized for the specified provider/model. "
|
||||
"Return STRICT JSON matching the provided schema, no extra text.\n\n"
|
||||
+ (
|
||||
"TEXT RENDERING: The current model CAN render readable text. "
|
||||
"Include the section title or a key phrase (1-8 words) as part of the generated image. "
|
||||
"Integrate text naturally as a headline, label, or typographic element."
|
||||
if can_render_text
|
||||
else "TEXT RENDERING: The image model CANNOT render readable text. "
|
||||
"Never ask it to generate text. Design clean, high-contrast overlay-safe zones instead."
|
||||
)
|
||||
)
|
||||
|
||||
# Get model-specific guidance
|
||||
@@ -698,40 +660,57 @@ def suggest_prompts(
|
||||
"wavespeed": "Blog-optimized imagery: focus on data visualization, infographics, clean layouts with text overlay areas, professional diagrams, charts, or conceptual illustrations. Avoid random people or poster-style images. Prefer clean backgrounds suitable for text overlays, data representations, or abstract concepts that support the blog content."
|
||||
}.get(provider, "")
|
||||
|
||||
# Combine provider and model-specific guidance
|
||||
# Combine provider and model-specific guidance (model guidance is primary)
|
||||
provider_guidance = provider_guidance_base
|
||||
if model_guidance_text:
|
||||
provider_guidance = f"{provider_guidance_base}\n\nMODEL-SPECIFIC GUIDANCE ({model}): {model_guidance_text}"
|
||||
parts = [
|
||||
f"PROVIDER: {provider} / Model: {model or 'auto-selected'}",
|
||||
f"MODEL GUIDANCE: {model_guidance_text}"
|
||||
]
|
||||
if model_best_practices:
|
||||
provider_guidance += f"\nBest Practices:\n" + "\n".join([f"- {bp}" for bp in model_best_practices])
|
||||
parts.append("Best Practices:\n" + "\n".join([f"- {bp}" for bp in model_best_practices]))
|
||||
if model_warnings:
|
||||
provider_guidance += f"\n⚠️ WARNINGS:\n" + "\n".join([f"- {w}" for w in model_warnings])
|
||||
parts.append("WARNINGS:\n" + "\n".join([f"- {w}" for w in model_warnings]))
|
||||
if provider_guidance_base:
|
||||
parts.append(f"Provider context ({provider}): {provider_guidance_base}")
|
||||
provider_guidance = "\n\n".join(parts)
|
||||
|
||||
best_practices = (
|
||||
"BLOG IMAGE BEST PRACTICES: Create images optimized for blog content, not social media posters. "
|
||||
"Focus on: data visualization elements (charts, graphs, infographics), clean layouts with designated text overlay areas, "
|
||||
"professional diagrams, conceptual illustrations, or abstract representations of the topic. "
|
||||
"Avoid: random people posing, poster-style compositions, busy social media graphics, or trying to recreate text/words as images. "
|
||||
"Instead: use clean backgrounds, simple compositions, areas reserved for text overlays, data-driven visuals, or conceptual imagery. "
|
||||
"Technical: one clear focal subject; clean, uncluttered background; text-safe margins (20% padding on all sides for overlays); "
|
||||
"neutral or professional lighting; avoid busy patterns; no brand logos or watermarks; no copyrighted characters; "
|
||||
"avoid low-res, blur, noise, banding, oversaturation, over-sharpening; prefer 1024px+ on shortest side for quality."
|
||||
"BLOG IMAGE BEST PRACTICES: "
|
||||
+ (
|
||||
"Create professional blog images with clear typography. "
|
||||
"Include text elements (headlines, labels) naturally in the design. "
|
||||
"Use clean compositions with strong visual hierarchy. "
|
||||
"Avoid: busy patterns, brand logos, watermarks, low resolution."
|
||||
if can_render_text
|
||||
else (
|
||||
"Design for text overlay — use clean backgrounds with designated text zones (20% padding). "
|
||||
"Focus on abstract representations, data metaphors, or conceptual imagery. "
|
||||
"NEVER include text, words, letters, numbers, or labels in the generated image. "
|
||||
"Avoid: busy patterns, brand logos, watermarks, low resolution."
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
overlay_hint = (
|
||||
"IMPORTANT FOR BLOG IMAGES: Design images with text overlay areas in mind. "
|
||||
"Include space for headlines, captions, or data labels. "
|
||||
"Suggest overlay_text (short title or key statistic, <= 8 words) that would work well as a text overlay. "
|
||||
"Ensure clean, high-contrast safe areas (top 20% or bottom 20% of image) for text placement. "
|
||||
"The image should complement text, not replace it - think data visualization, infographics, or clean conceptual imagery."
|
||||
if (req.include_overlay is None or req.include_overlay)
|
||||
else "Do not include on-image text, but still design with text overlay areas in mind for blog use."
|
||||
(
|
||||
"Include the section title or key phrase IN the generated image as a typographic element (headline, label, etc.). "
|
||||
"Keep text minimal: 1-8 words."
|
||||
if can_render_text
|
||||
else (
|
||||
"ABSOLUTELY FORBIDDEN: The image model CANNOT render text. "
|
||||
"Design with clean, high-contrast safe zones (top 20% or bottom 20%) for HTML overlay text. "
|
||||
"Suggest overlay_text (short title or key statistic, <= 8 words) that works as a text overlay."
|
||||
if (req.include_overlay is None or req.include_overlay)
|
||||
else "Do not include on-image text, but still design with text overlay areas in mind."
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Image type specific guidance (enhanced with infographic type)
|
||||
image_type_guidance = {
|
||||
"realistic": "Photorealistic style with professional photography quality. Include camera settings and lighting details.",
|
||||
"chart": "⚠️ IMPORTANT: Complex infographics are too difficult for current AI models. Create simple visual representations with designated text overlay areas instead. Use abstract data visualization elements, not actual charts with embedded text.",
|
||||
"chart": "⚠️ FORBIDDEN: Do NOT create actual charts, graphs, or data visualizations with embedded text. The image model cannot render readable labels or data points. Instead, create abstract visual metaphors for data — flowing shapes, color gradients, connected nodes, layered elements, or geometric patterns that evoke the data concept. Design with text overlay zones for data labels that will be added as HTML overlay.",
|
||||
"conceptual": "Abstract or conceptual imagery that represents the topic visually. Clean compositions with text overlay zones.",
|
||||
"diagram": "Technical diagrams with simple, clear visual elements. Design for text overlay areas, not embedded labels.",
|
||||
"illustration": "Stylized illustrations that support the content. Professional, clean aesthetic suitable for blog use.",
|
||||
@@ -780,31 +759,31 @@ def suggest_prompts(
|
||||
8. Are optimized for blog article use (not social media)
|
||||
|
||||
PROMPT QUALITY REQUIREMENTS:
|
||||
- Each prompt should be specific and detailed (50-100 words)
|
||||
- Use the visual data intelligently - prioritize statistics and data points for charts, concepts for conceptual images
|
||||
- Include visual composition guidance (layout, colors, style)
|
||||
- Each prompt should be concise (20-40 words)
|
||||
- Focus on visual composition, style, and key visual elements
|
||||
- Specify lighting and quality descriptors when appropriate
|
||||
- Make prompts actionable and clear for the AI model
|
||||
|
||||
NEGATIVE PROMPT:
|
||||
Include a suitable negative_prompt that excludes: people posing, social media graphics, posters, text rendered as images, busy compositions, watermarks, logos{f", {negative_prompt_additions}" if negative_prompt_additions else ""}.
|
||||
|
||||
DIMENSIONS:
|
||||
Suggest width/height when relevant (e.g., 1024x1024 for square, 1920x1080 for landscape blog headers).
|
||||
Default to 1024x1024 for consistent blog image format. Do NOT reference specific pixel dimensions in the prompt text.
|
||||
|
||||
OVERLAY TEXT:
|
||||
If including overlay text suggestion, return it in overlay_text (short: <= 8 words, typically a key statistic or section title). Use statistics from the visual data when available.
|
||||
{("Include the overlay_text IN the generated image as a typographic element (headline, label, etc.) — "
|
||||
"it will be rendered as part of the image. Keep it minimal: 1-8 words (key statistic or section title). "
|
||||
"Use statistics from the visual data when available.")
|
||||
if can_render_text else
|
||||
("Suggest overlay_text (short: <= 8 words, typically a key statistic or section title) as metadata only — "
|
||||
"it will be rendered as HTML overlay. Do NOT include text in the image. "
|
||||
"Use statistics from the visual data when available.")}
|
||||
"""
|
||||
|
||||
# Get user_id for llm_text_gen subscription check (required)
|
||||
if not current_user:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
|
||||
user_id_for_llm = str(current_user.get('id', ''))
|
||||
if not user_id_for_llm:
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="Invalid user ID in authentication token")
|
||||
|
||||
raw = llm_text_gen(prompt=prompt, system_prompt=system, json_struct=schema, user_id=user_id_for_llm)
|
||||
raw = llm_text_gen(prompt=prompt, system_prompt=system, json_struct=schema, user_id=user_id)
|
||||
data = raw if isinstance(raw, dict) else {}
|
||||
suggestions = data.get("suggestions") or []
|
||||
# basic fallback if provider returns string
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends, status
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import os
|
||||
from loguru import logger
|
||||
@@ -22,9 +22,18 @@ from api.content_planning.services.content_strategy.onboarding import Onboarding
|
||||
from models.onboarding import SEOPageAudit, WebsiteAnalysis, OnboardingSession
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
from sqlalchemy import desc
|
||||
|
||||
# Phase 2B: Import semantic monitoring
|
||||
from services.intelligence.monitoring.semantic_dashboard import RealTimeSemanticMonitor, SemanticHealthMetric
|
||||
|
||||
# GSC services for keyword gap analysis
|
||||
from services.gsc_service import GSCService
|
||||
from services.gsc_brainstorm_service import GSCBrainstormService
|
||||
|
||||
# Import SIF models for guardian audit
|
||||
from models.website_analysis_monitoring_models import SIFIndexingTask, SIFIndexingExecutionLog
|
||||
|
||||
router = APIRouter(prefix="/api/seo-dashboard", tags=["SEO Dashboard"])
|
||||
|
||||
# Initialize the SEO analyzer
|
||||
@@ -577,6 +586,172 @@ async def get_sif_indexing_health(current_user: dict = Depends(get_current_user)
|
||||
raise HTTPException(status_code=500, detail="Failed to get SIF indexing health")
|
||||
|
||||
|
||||
async def get_guardian_audit(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the latest Content Guardian audit report for the current user.
|
||||
Returns audit data (quality, brand voice, safety, cannibalization) or a
|
||||
null-state response if no audit has been performed yet.
|
||||
"""
|
||||
try:
|
||||
user_id = str(current_user.get("id"))
|
||||
db_session = get_session_for_user(user_id)
|
||||
if not db_session:
|
||||
raise HTTPException(status_code=500, detail="Database connection unavailable")
|
||||
|
||||
try:
|
||||
# Find the most recent SIF indexing task for this user
|
||||
task = (
|
||||
db_session.query(SIFIndexingTask)
|
||||
.filter(SIFIndexingTask.user_id == user_id)
|
||||
.order_by(desc(SIFIndexingTask.created_at))
|
||||
.first()
|
||||
)
|
||||
|
||||
if not task:
|
||||
return {
|
||||
"has_audit": False,
|
||||
"status": "not_available",
|
||||
"message": "No SIF indexing task found. Onboarding may not be complete.",
|
||||
}
|
||||
|
||||
# Get the latest execution log with a guardian report
|
||||
log = (
|
||||
db_session.query(SIFIndexingExecutionLog)
|
||||
.filter(
|
||||
SIFIndexingExecutionLog.task_id == task.id,
|
||||
SIFIndexingExecutionLog.result_data.isnot(None),
|
||||
)
|
||||
.order_by(desc(SIFIndexingExecutionLog.execution_date))
|
||||
.first()
|
||||
)
|
||||
|
||||
if not log or not log.result_data:
|
||||
return {
|
||||
"has_audit": False,
|
||||
"status": "pending",
|
||||
"message": "SIF indexing has not completed a run yet.",
|
||||
}
|
||||
|
||||
guardian_report = log.result_data.get("guardian_report")
|
||||
if not guardian_report:
|
||||
return {
|
||||
"has_audit": False,
|
||||
"status": "no_report",
|
||||
"message": "Guardian audit was not performed on the last indexing run.",
|
||||
}
|
||||
|
||||
return {
|
||||
"has_audit": True,
|
||||
"status": "available",
|
||||
"audit_timestamp": guardian_report.get("audit_timestamp"),
|
||||
"website_url": guardian_report.get("website_url"),
|
||||
"total_pages_crawled": guardian_report.get("total_pages_crawled", 0),
|
||||
"content_quality": guardian_report.get("content_quality"),
|
||||
"brand_voice_consistency": guardian_report.get("brand_voice_consistency"),
|
||||
"safety_issues": guardian_report.get("safety_issues"),
|
||||
"cannibalization_issues": guardian_report.get("cannibalization_issues"),
|
||||
"last_execution_time": log.execution_date.isoformat() if log.execution_date else None,
|
||||
}
|
||||
finally:
|
||||
db_session.close()
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get guardian audit: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to get guardian audit")
|
||||
|
||||
|
||||
async def get_keyword_gaps(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
site_url: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get keyword gap analysis from GSC data.
|
||||
Returns keyword gaps, quick wins, content opportunities, and page-level opportunities
|
||||
derived from the user's Google Search Console search analytics (last 30 days).
|
||||
"""
|
||||
try:
|
||||
user_id = str(current_user.get("id"))
|
||||
|
||||
gsc_service = GSCService()
|
||||
brainstorm_service = GSCBrainstormService(gsc_service)
|
||||
|
||||
# Resolve site URL
|
||||
if not site_url:
|
||||
sites = gsc_service.get_site_list(user_id)
|
||||
if not sites:
|
||||
return {
|
||||
"error": "No GSC sites found. Connect Google Search Console first.",
|
||||
"keyword_gaps": [],
|
||||
"quick_wins": [],
|
||||
"content_opportunities": [],
|
||||
"page_opportunities": [],
|
||||
"summary": {},
|
||||
}
|
||||
site_url = sites[0].get("siteUrl", "")
|
||||
|
||||
# Fetch GSC analytics (last 30 days)
|
||||
end_date = datetime.now().strftime("%Y-%m-%d")
|
||||
start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
|
||||
|
||||
analytics = gsc_service.get_search_analytics(
|
||||
user_id=user_id,
|
||||
site_url=site_url,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
)
|
||||
|
||||
if "error" in analytics:
|
||||
return {
|
||||
"error": analytics.get("error", "Failed to fetch GSC data"),
|
||||
"keyword_gaps": [],
|
||||
"quick_wins": [],
|
||||
"content_opportunities": [],
|
||||
"page_opportunities": [],
|
||||
"summary": {},
|
||||
}
|
||||
|
||||
query_rows = analytics.get("query_data", {}).get("rows", [])
|
||||
page_rows = analytics.get("page_data", {}).get("rows", [])
|
||||
|
||||
keywords_data = GSCBrainstormService._parse_query_rows(query_rows)
|
||||
pages_data = GSCBrainstormService._parse_page_rows(page_rows)
|
||||
|
||||
if not keywords_data:
|
||||
return {
|
||||
"error": "No keyword data available for the last 30 days.",
|
||||
"keyword_gaps": [],
|
||||
"quick_wins": [],
|
||||
"content_opportunities": [],
|
||||
"page_opportunities": [],
|
||||
"summary": {
|
||||
"site_url": site_url,
|
||||
"date_range": {"start": start_date, "end": end_date},
|
||||
"total_keywords_analyzed": 0,
|
||||
},
|
||||
}
|
||||
|
||||
# Run rule-based analysis WITHOUT topic filter (site-wide)
|
||||
content_opportunities = GSCBrainstormService._identify_content_opportunities(keywords_data)
|
||||
keyword_gaps = GSCBrainstormService._identify_keyword_gaps(keywords_data)
|
||||
quick_wins = GSCBrainstormService._identify_quick_wins(keywords_data)
|
||||
page_opportunities = GSCBrainstormService._identify_page_opportunities(pages_data)
|
||||
summary = GSCBrainstormService._compute_summary(
|
||||
keywords_data, pages_data, site_url, start_date, end_date
|
||||
)
|
||||
|
||||
return {
|
||||
"keyword_gaps": keyword_gaps,
|
||||
"quick_wins": quick_wins,
|
||||
"content_opportunities": content_opportunities,
|
||||
"page_opportunities": page_opportunities,
|
||||
"summary": summary,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get keyword gaps: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get keyword gaps: {str(e)}")
|
||||
|
||||
|
||||
async def get_onboarding_task_health(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
site_url: Optional[str] = None,
|
||||
|
||||
@@ -1,68 +1,19 @@
|
||||
"""
|
||||
Cache management for subscription API endpoints.
|
||||
|
||||
Delegates to the canonical implementation in services/subscription/cache.py.
|
||||
All cache state lives there so service-layer code can invalidate without
|
||||
importing from the API layer.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
import time
|
||||
import os
|
||||
from services.subscription.cache import (
|
||||
get_cached_dashboard,
|
||||
set_cached_dashboard,
|
||||
clear_dashboard_cache,
|
||||
)
|
||||
|
||||
|
||||
# Simple in-process cache for dashboard responses to smooth bursts
|
||||
# Cache key: (user_id). TTL-like behavior implemented via timestamp check
|
||||
_dashboard_cache: Dict[str, Dict[str, Any]] = {}
|
||||
_dashboard_cache_ts: Dict[str, float] = {}
|
||||
_DASHBOARD_CACHE_TTL_SEC = 600.0
|
||||
|
||||
|
||||
def get_cached_dashboard(user_id: str) -> Dict[str, Any] | None:
|
||||
"""
|
||||
Get cached dashboard data if available and not expired.
|
||||
|
||||
Args:
|
||||
user_id: User ID to get cached data for
|
||||
|
||||
Returns:
|
||||
Cached dashboard data or None if not cached/expired
|
||||
"""
|
||||
# Check if caching is disabled via environment variable
|
||||
nocache = False
|
||||
try:
|
||||
nocache = os.getenv('SUBSCRIPTION_DASHBOARD_NOCACHE', 'false').lower() in {'1', 'true', 'yes', 'on'}
|
||||
except Exception:
|
||||
nocache = False
|
||||
|
||||
if nocache:
|
||||
return None
|
||||
|
||||
now = time.time()
|
||||
if user_id in _dashboard_cache and (now - _dashboard_cache_ts.get(user_id, 0)) < _DASHBOARD_CACHE_TTL_SEC:
|
||||
return _dashboard_cache[user_id]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def set_cached_dashboard(user_id: str, data: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Cache dashboard data for a user.
|
||||
|
||||
Args:
|
||||
user_id: User ID to cache data for
|
||||
data: Dashboard data to cache
|
||||
"""
|
||||
_dashboard_cache[user_id] = data
|
||||
_dashboard_cache_ts[user_id] = time.time()
|
||||
|
||||
|
||||
def clear_dashboard_cache(user_id: str | None = None) -> None:
|
||||
"""
|
||||
Clear dashboard cache for a specific user or all users.
|
||||
|
||||
Args:
|
||||
user_id: User ID to clear cache for, or None to clear all
|
||||
"""
|
||||
if user_id:
|
||||
_dashboard_cache.pop(user_id, None)
|
||||
_dashboard_cache_ts.pop(user_id, None)
|
||||
else:
|
||||
_dashboard_cache.clear()
|
||||
_dashboard_cache_ts.clear()
|
||||
__all__ = [
|
||||
"get_cached_dashboard",
|
||||
"set_cached_dashboard",
|
||||
"clear_dashboard_cache",
|
||||
]
|
||||
|
||||
@@ -109,48 +109,49 @@ async def preflight_check(
|
||||
|
||||
# Get pricing for this operation
|
||||
model_name = op.get('model')
|
||||
pricing_info = None
|
||||
if model_name:
|
||||
pricing_info = pricing_service.get_pricing_for_provider_model(
|
||||
op['provider'],
|
||||
model_name
|
||||
)
|
||||
|
||||
if pricing_info:
|
||||
# Determine cost based on operation type
|
||||
if op['provider'] in [APIProvider.VIDEO, APIProvider.IMAGE_EDIT, APIProvider.STABILITY]:
|
||||
cost = pricing_info.get('cost_per_request', 0.0) or pricing_info.get('cost_per_image', 0.0) or 0.0
|
||||
elif op['provider'] == APIProvider.AUDIO:
|
||||
model_lower = (model_name or "").lower()
|
||||
if model_lower == "minimax/voice-clone":
|
||||
cost = pricing_info.get('cost_per_request', 0.5) or 0.5
|
||||
elif model_lower == "wavespeed-ai/qwen3-tts/voice-clone":
|
||||
chars = max(0, int(op.get('tokens_requested') or 0))
|
||||
cost = max(0.005, 0.005 * (chars / 100.0))
|
||||
else:
|
||||
cost = (pricing_info.get('cost_per_input_token', 0.0) or 0.0) * op['tokens_requested']
|
||||
elif op['tokens_requested'] > 0:
|
||||
cost = (pricing_info.get('cost_per_input_token', 0.0) or 0.0) * op['tokens_requested']
|
||||
|
||||
if pricing_info:
|
||||
# Determine cost based on operation type
|
||||
if op['provider'] in [APIProvider.VIDEO, APIProvider.IMAGE_EDIT, APIProvider.STABILITY]:
|
||||
cost = pricing_info.get('cost_per_request', 0.0) or pricing_info.get('cost_per_image', 0.0) or 0.0
|
||||
elif op['provider'] == APIProvider.AUDIO:
|
||||
model_lower = (model_name or "").lower()
|
||||
if model_lower == "minimax/voice-clone":
|
||||
cost = pricing_info.get('cost_per_request', 0.5) or 0.5
|
||||
elif model_lower == "wavespeed-ai/qwen3-tts/voice-clone":
|
||||
chars = max(0, int(op.get('tokens_requested') or 0))
|
||||
cost = max(0.005, 0.005 * (chars / 100.0))
|
||||
else:
|
||||
cost = pricing_info.get('cost_per_request', 0.0) or 0.0
|
||||
|
||||
cost = (pricing_info.get('cost_per_input_token', 0.0) or 0.0) * op['tokens_requested']
|
||||
elif op['tokens_requested'] > 0:
|
||||
cost = (pricing_info.get('cost_per_input_token', 0.0) or 0.0) * op['tokens_requested']
|
||||
else:
|
||||
cost = pricing_info.get('cost_per_request', 0.0) or 0.0
|
||||
|
||||
op_result['cost'] = round(cost, 4)
|
||||
total_cost += cost
|
||||
else:
|
||||
# Use default cost if pricing not found or no model specified
|
||||
if op['provider'] == APIProvider.VIDEO:
|
||||
op_result['cost'] = 0.10 # Default video cost
|
||||
total_cost += 0.10
|
||||
elif op['provider'] == APIProvider.IMAGE_EDIT:
|
||||
op_result['cost'] = 0.05 # Default image edit cost
|
||||
total_cost += 0.05
|
||||
elif op['provider'] == APIProvider.STABILITY:
|
||||
op_result['cost'] = 0.04 # Default image generation cost
|
||||
total_cost += 0.04
|
||||
elif op['provider'] == APIProvider.AUDIO:
|
||||
# Default audio cost: $0.05 per 1,000 characters
|
||||
cost = (op['tokens_requested'] / 1000.0) * 0.05
|
||||
op_result['cost'] = round(cost, 4)
|
||||
total_cost += cost
|
||||
else:
|
||||
# Use default cost if pricing not found
|
||||
if op['provider'] == APIProvider.VIDEO:
|
||||
op_result['cost'] = 0.10 # Default video cost
|
||||
total_cost += 0.10
|
||||
elif op['provider'] == APIProvider.IMAGE_EDIT:
|
||||
op_result['cost'] = 0.05 # Default image edit cost
|
||||
total_cost += 0.05
|
||||
elif op['provider'] == APIProvider.STABILITY:
|
||||
op_result['cost'] = 0.04 # Default image generation cost
|
||||
total_cost += 0.04
|
||||
elif op['provider'] == APIProvider.AUDIO:
|
||||
# Default audio cost: $0.05 per 1,000 characters
|
||||
cost = (op['tokens_requested'] / 1000.0) * 0.05
|
||||
op_result['cost'] = round(cost, 4)
|
||||
total_cost += cost
|
||||
|
||||
# Get limit information
|
||||
limit_info = None
|
||||
|
||||
169
backend/api/youtube/oauth_router.py
Normal file
169
backend/api/youtube/oauth_router.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""
|
||||
YouTube OAuth Router
|
||||
Handles YouTube Data API v3 OAuth2 authentication flow.
|
||||
Uses shared build_oauth_callback_html for popup-compatible callback responses.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from loguru import logger
|
||||
|
||||
from middleware.auth_middleware import get_current_user, get_optional_user
|
||||
from services.youtube.youtube_oauth_service import YouTubeOAuthService
|
||||
from services.integrations.oauth_callback_utils import build_oauth_callback_html
|
||||
|
||||
router = APIRouter(prefix="/youtube/oauth", tags=["youtube-oauth"])
|
||||
|
||||
|
||||
def get_oauth_service() -> YouTubeOAuthService:
|
||||
try:
|
||||
return YouTubeOAuthService()
|
||||
except ValueError as e:
|
||||
logger.error(f"YouTube OAuth service init failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/auth/url")
|
||||
def get_youtube_auth_url(
|
||||
user: dict = Depends(get_current_user),
|
||||
service: YouTubeOAuthService = Depends(get_oauth_service),
|
||||
):
|
||||
"""Generate YouTube OAuth authorization URL. Frontend opens this in a popup."""
|
||||
try:
|
||||
user_id = user.get("id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
|
||||
auth_url = service.generate_authorization_url(user_id)
|
||||
if not auth_url:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Failed to generate authorization URL. Check server logs.",
|
||||
)
|
||||
|
||||
logger.info(f"YouTube OAuth URL generated for user {user_id}")
|
||||
return {"auth_url": auth_url}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating YouTube auth URL: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/callback")
|
||||
def handle_youtube_callback(
|
||||
code: str = Query(None),
|
||||
state: str = Query(None),
|
||||
error: str = Query(None),
|
||||
request: Request = None,
|
||||
service: YouTubeOAuthService = Depends(get_oauth_service),
|
||||
):
|
||||
"""
|
||||
Handle OAuth callback from Google.
|
||||
|
||||
Returns HTML with postMessage to the opener popup window (GSC/WordPress pattern).
|
||||
Supports JSON response via ?format=json for server-side flows.
|
||||
"""
|
||||
# User denied authorization
|
||||
if error:
|
||||
logger.warning(f"YouTube OAuth: user denied authorization: {error}")
|
||||
html = build_oauth_callback_html(
|
||||
payload={"type": "YOUTUBE_OAUTH_ERROR", "error": error},
|
||||
title="Authorization Denied",
|
||||
heading="Authorization Denied",
|
||||
message=f"You denied the authorization request. {error}",
|
||||
)
|
||||
return _response_as_html(request, html)
|
||||
|
||||
# Validate parameters
|
||||
if not code or not state:
|
||||
logger.error("YouTube OAuth: missing code or state parameters")
|
||||
html = build_oauth_callback_html(
|
||||
payload={"type": "YOUTUBE_OAUTH_ERROR", "error": "Missing authorization code or state"},
|
||||
title="Authorization Failed",
|
||||
heading="Missing Parameters",
|
||||
message="The authorization request was missing required parameters. Please try again.",
|
||||
)
|
||||
return _response_as_html(request, html)
|
||||
|
||||
# Exchange code for tokens
|
||||
result = service.handle_oauth_callback(authorization_code=code, state=state)
|
||||
|
||||
if result.get("success"):
|
||||
channel_name = result.get("channel_name", "your channel")
|
||||
html = build_oauth_callback_html(
|
||||
payload={
|
||||
"type": "YOUTUBE_OAUTH_SUCCESS",
|
||||
"channel_id": result.get("channel_id", ""),
|
||||
"channel_name": channel_name,
|
||||
},
|
||||
title="YouTube Connected",
|
||||
heading="YouTube Connected!",
|
||||
message=f"Successfully connected to {channel_name}. You can now close this window.",
|
||||
)
|
||||
logger.info(f"YouTube OAuth callback succeeded for channel: {channel_name}")
|
||||
return _response_as_html(request, html)
|
||||
|
||||
error_msg = result.get("error", "Unknown error during authorization")
|
||||
logger.error(f"YouTube OAuth callback failed: {error_msg}")
|
||||
html = build_oauth_callback_html(
|
||||
payload={"type": "YOUTUBE_OAUTH_ERROR", "error": error_msg},
|
||||
title="Connection Failed",
|
||||
heading="Connection Failed",
|
||||
message=f"Failed to connect YouTube: {error_msg}. Please try again.",
|
||||
)
|
||||
return _response_as_html(request, html)
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
def get_youtube_status(
|
||||
user: dict = Depends(get_current_user),
|
||||
service: YouTubeOAuthService = Depends(get_oauth_service),
|
||||
):
|
||||
"""Check YouTube connection status for the authenticated user."""
|
||||
try:
|
||||
user_id = user.get("id")
|
||||
status = service.get_connection_status(user_id)
|
||||
return {"success": True, **status}
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking YouTube OAuth status: {e}")
|
||||
return {"success": False, "connected": False, "channels": [], "error": str(e)}
|
||||
|
||||
|
||||
@router.delete("/disconnect/{token_id}")
|
||||
def disconnect_youtube(
|
||||
token_id: int,
|
||||
user: dict = Depends(get_current_user),
|
||||
service: YouTubeOAuthService = Depends(get_oauth_service),
|
||||
):
|
||||
"""Deactivate a YouTube OAuth token."""
|
||||
try:
|
||||
user_id = user.get("id")
|
||||
result = service.revoke_token(user_id, token_id)
|
||||
if result:
|
||||
return {"success": True, "message": "YouTube disconnected"}
|
||||
return {"success": False, "message": "Failed to disconnect"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error disconnecting YouTube: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
def _response_as_html(request: Request, html: str):
|
||||
"""Return HTML response, or JSON if ?format=json is present."""
|
||||
if request and request.query_params.get("format") == "json":
|
||||
from fastapi.responses import JSONResponse
|
||||
import json as json_lib
|
||||
|
||||
# Extract payload from HTML for JSON response
|
||||
try:
|
||||
payload_start = html.index('"type":')
|
||||
payload_end = html.index("</script>", payload_start)
|
||||
snippet = html[payload_start : payload_end - 3]
|
||||
payload = json_lib.loads("{" + snippet + "}")
|
||||
return JSONResponse(content=payload)
|
||||
except Exception:
|
||||
return JSONResponse(content={"success": False, "error": "OAuth processing completed"})
|
||||
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
return HTMLResponse(content=html, headers={"Cross-Origin-Opener-Policy": "unsafe-none"})
|
||||
218
backend/api/youtube/publish_router.py
Normal file
218
backend/api/youtube/publish_router.py
Normal file
@@ -0,0 +1,218 @@
|
||||
"""
|
||||
YouTube Publish Router
|
||||
Handles video upload/publishing to YouTube via the Data API v3.
|
||||
Uses stored OAuth credentials for authentication.
|
||||
"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from loguru import logger
|
||||
|
||||
from middleware.auth_middleware import get_current_user
|
||||
from services.youtube.youtube_oauth_service import YouTubeOAuthService
|
||||
from services.youtube.youtube_publish_service import YouTubePublishService
|
||||
from .oauth_router import get_oauth_service
|
||||
from .task_manager import task_manager
|
||||
|
||||
router = APIRouter(prefix="/youtube/publish", tags=["youtube-publish"])
|
||||
|
||||
|
||||
class PublishRequest(BaseModel):
|
||||
token_id: int = Field(..., description="YouTube OAuth token row ID (which channel to publish to)")
|
||||
video_source: str = Field(..., description="URL or local file path to the video")
|
||||
title: str = Field(..., min_length=1, max_length=100, description="Video title (max 100 chars)")
|
||||
description: str = Field("", description="Video description")
|
||||
tags: List[str] = Field(default_factory=list, description="Video tags")
|
||||
privacy_status: str = Field("unlisted", pattern="^(public|private|unlisted)$", description="Privacy status")
|
||||
category_id: str = Field("22", description="YouTube category ID (default: People & Blogs)")
|
||||
made_for_kids: bool = Field(False, description="Whether content is made for children")
|
||||
|
||||
|
||||
class PublishResponse(BaseModel):
|
||||
success: bool
|
||||
task_id: Optional[str] = None
|
||||
video_id: Optional[str] = None
|
||||
video_url: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
message: str = ""
|
||||
|
||||
|
||||
def get_publish_service(
|
||||
oauth_service: YouTubeOAuthService = Depends(get_oauth_service),
|
||||
) -> YouTubePublishService:
|
||||
return YouTubePublishService(oauth_service)
|
||||
|
||||
|
||||
@router.post("", response_model=PublishResponse)
|
||||
def start_publish(
|
||||
request: PublishRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
user: dict = Depends(get_current_user),
|
||||
publish_service: YouTubePublishService = Depends(get_publish_service),
|
||||
):
|
||||
"""Start publishing a video to YouTube as a background task."""
|
||||
try:
|
||||
user_id = user.get("id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
|
||||
# Verify token belongs to user
|
||||
oauth_service = publish_service.oauth_service
|
||||
status = oauth_service.get_connection_status(user_id)
|
||||
tokens = [c for c in status.get("channels", []) if c["token_id"] == request.token_id and c["is_active"]]
|
||||
if not tokens:
|
||||
raise HTTPException(status_code=400, detail="Invalid or inactive token_id")
|
||||
|
||||
# Create background task
|
||||
task_id = task_manager.create_task("youtube_publish")
|
||||
logger.info(
|
||||
f"YouTube publish: created task {task_id} for user {user_id}, "
|
||||
f"title='{request.title[:50]}', channel={tokens[0].get('channel_name', 'unknown')}"
|
||||
)
|
||||
|
||||
background_tasks.add_task(
|
||||
_execute_publish_task,
|
||||
task_id=task_id,
|
||||
user_id=user_id,
|
||||
token_id=request.token_id,
|
||||
video_source=request.video_source,
|
||||
title=request.title,
|
||||
description=request.description,
|
||||
tags=request.tags,
|
||||
privacy_status=request.privacy_status,
|
||||
category_id=request.category_id,
|
||||
made_for_kids=request.made_for_kids,
|
||||
publish_service=publish_service,
|
||||
)
|
||||
|
||||
return PublishResponse(
|
||||
success=True,
|
||||
task_id=task_id,
|
||||
message="Publishing to YouTube started. Poll task_id for progress.",
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"YouTube publish: error starting task: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{task_id}", response_model=PublishResponse)
|
||||
def get_publish_status(
|
||||
task_id: str,
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Check the status of a YouTube publish task."""
|
||||
try:
|
||||
user_id = user.get("id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
|
||||
task_status = task_manager.get_task_status(task_id)
|
||||
if not task_status:
|
||||
return PublishResponse(
|
||||
success=False,
|
||||
error="Task not found",
|
||||
message="Publish task not found (may have expired).",
|
||||
)
|
||||
|
||||
status = task_status.get("status", "unknown")
|
||||
result = task_status.get("result") or {}
|
||||
error = task_status.get("error")
|
||||
|
||||
if status == "completed":
|
||||
return PublishResponse(
|
||||
success=True,
|
||||
task_id=task_id,
|
||||
video_id=result.get("video_id"),
|
||||
video_url=result.get("video_url"),
|
||||
message=task_status.get("message", "Published successfully"),
|
||||
)
|
||||
elif status == "failed":
|
||||
return PublishResponse(
|
||||
success=False,
|
||||
task_id=task_id,
|
||||
error=error or result.get("error", "Publish failed"),
|
||||
message=task_status.get("message", "Publish failed"),
|
||||
)
|
||||
else:
|
||||
return PublishResponse(
|
||||
success=False,
|
||||
task_id=task_id,
|
||||
message=task_status.get("message", "Publishing in progress..."),
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"YouTube publish: status check error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
def _execute_publish_task(
|
||||
task_id: str,
|
||||
user_id: str,
|
||||
token_id: int,
|
||||
video_source: str,
|
||||
title: str,
|
||||
description: str,
|
||||
tags: List[str],
|
||||
privacy_status: str,
|
||||
category_id: str,
|
||||
made_for_kids: bool,
|
||||
publish_service: YouTubePublishService,
|
||||
):
|
||||
"""Background task to execute video publish."""
|
||||
logger.info(f"YouTube publish: background task {task_id} starting for user {user_id}")
|
||||
|
||||
try:
|
||||
task_manager.update_task_status(
|
||||
task_id, "processing", progress=10.0, message="Preparing video for upload..."
|
||||
)
|
||||
|
||||
result = publish_service.publish_video(
|
||||
user_id=user_id,
|
||||
token_id=token_id,
|
||||
video_source=video_source,
|
||||
title=title,
|
||||
description=description,
|
||||
tags=tags,
|
||||
privacy_status=privacy_status,
|
||||
category_id=category_id,
|
||||
made_for_kids=made_for_kids,
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
task_manager.update_task_status(
|
||||
task_id,
|
||||
"completed",
|
||||
progress=100.0,
|
||||
message=f"Published successfully: {result.get('video_url', '')}",
|
||||
result=result,
|
||||
)
|
||||
logger.info(
|
||||
f"YouTube publish: task {task_id} completed — "
|
||||
f"video_id={result.get('video_id')}, url={result.get('video_url')}"
|
||||
)
|
||||
else:
|
||||
error_msg = result.get("error", "Unknown publish error")
|
||||
logger.error(f"YouTube publish: task {task_id} failed: {error_msg}")
|
||||
task_manager.update_task_status(
|
||||
task_id,
|
||||
"failed",
|
||||
error=error_msg,
|
||||
message="Publish failed",
|
||||
result=result,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"YouTube publish: background task {task_id} error: {e}")
|
||||
task_manager.update_task_status(
|
||||
task_id,
|
||||
"failed",
|
||||
error=str(e),
|
||||
message="Publish error",
|
||||
result={"error": str(e)},
|
||||
)
|
||||
@@ -30,6 +30,8 @@ from .task_manager import task_manager
|
||||
from .handlers import avatar as avatar_handlers
|
||||
from .handlers import images as image_handlers
|
||||
from .handlers import audio as audio_handlers
|
||||
from .oauth_router import router as youtube_oauth_router
|
||||
from .publish_router import router as youtube_publish_router
|
||||
|
||||
router = APIRouter(prefix="/youtube", tags=["youtube"])
|
||||
logger = get_service_logger("api.youtube")
|
||||
@@ -41,10 +43,12 @@ from .paths import (
|
||||
ensure_youtube_media_dirs,
|
||||
)
|
||||
|
||||
# Include sub-routers for avatar, images, and audio
|
||||
# Include sub-routers for avatar, images, audio, and OAuth
|
||||
router.include_router(avatar_handlers.router)
|
||||
router.include_router(image_handlers.router)
|
||||
router.include_router(audio_handlers.router)
|
||||
router.include_router(youtube_oauth_router)
|
||||
router.include_router(youtube_publish_router)
|
||||
|
||||
|
||||
# Request/Response Models
|
||||
|
||||
Reference in New Issue
Block a user