ALwrity version 0.5.4

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

View File

@@ -10,7 +10,7 @@ from sqlalchemy.orm import Session
from loguru import logger
import json
import asyncio
from datetime import datetime
from datetime import datetime, timedelta
from collections import defaultdict
import time
@@ -20,6 +20,7 @@ from services.database import get_db_session
# Import services
from ..services.enhanced_strategy_service import EnhancedStrategyService
from ..services.enhanced_strategy_db_service import EnhancedStrategyDBService
from ..services.content_strategy.autofill.ai_refresh import AutoFillRefreshService
# Import models
from models.enhanced_strategy_models import EnhancedContentStrategy
@@ -156,25 +157,7 @@ async def stream_strategic_intelligence(
yield {"type": "progress", "message": "Analyzing market positioning...", "progress": 40}
if strategies_data.get("status") == "not_found":
# Send fallback data
fallback_data = {
"market_positioning": {
"score": 75,
"strengths": ["Strong brand voice", "Consistent content quality"],
"weaknesses": ["Limited video content", "Slow content production"]
},
"competitive_advantages": [
{"advantage": "AI-powered content creation", "impact": "High", "implementation": "In Progress"},
{"advantage": "Data-driven strategy", "impact": "Medium", "implementation": "Complete"}
],
"strategic_risks": [
{"risk": "Content saturation in market", "probability": "Medium", "impact": "High"},
{"risk": "Algorithm changes affecting reach", "probability": "High", "impact": "Medium"}
]
}
# Cache the fallback data
set_cached_data(cache_key, fallback_data)
yield {"type": "result", "status": "success", "data": fallback_data, "progress": 100}
yield {"type": "error", "status": "not_ready", "message": "No strategies found. Complete onboarding and create a strategy before generating intelligence.", "progress": 100}
return
# Extract strategic intelligence from first strategy
@@ -274,34 +257,7 @@ async def stream_keyword_research(
# Handle case where gap_analyses is 0, None, or empty
if not gap_analyses or gap_analyses == 0 or len(gap_analyses) == 0:
# Send fallback data
fallback_data = {
"trend_analysis": {
"high_volume_keywords": [
{"keyword": "AI marketing automation", "volume": "10K-100K", "difficulty": "Medium"},
{"keyword": "content strategy 2024", "volume": "1K-10K", "difficulty": "Low"},
{"keyword": "digital marketing trends", "volume": "10K-100K", "difficulty": "High"}
],
"trending_keywords": [
{"keyword": "AI content generation", "growth": "+45%", "opportunity": "High"},
{"keyword": "voice search optimization", "growth": "+32%", "opportunity": "Medium"},
{"keyword": "video marketing strategy", "growth": "+28%", "opportunity": "High"}
]
},
"intent_analysis": {
"informational": ["how to", "what is", "guide to"],
"navigational": ["company name", "brand name", "website"],
"transactional": ["buy", "purchase", "download", "sign up"]
},
"opportunities": [
{"keyword": "AI content tools", "search_volume": "5K-10K", "competition": "Low", "cpc": "$2.50"},
{"keyword": "content marketing ROI", "search_volume": "1K-5K", "competition": "Medium", "cpc": "$4.20"},
{"keyword": "social media strategy", "search_volume": "10K-50K", "competition": "High", "cpc": "$3.80"}
]
}
# Cache the fallback data
set_cached_data(cache_key, fallback_data)
yield {"type": "result", "status": "success", "data": fallback_data, "progress": 100}
yield {"type": "error", "status": "not_ready", "message": "No keyword research data available. Connect data sources or run analysis first.", "progress": 100}
return
# Extract keyword data from first gap analysis
@@ -898,4 +854,157 @@ async def regenerate_enhanced_strategy_ai_analysis(
raise
except Exception as e:
logger.error(f"❌ Error regenerating AI analysis: {str(e)}")
raise ContentPlanningErrorHandler.handle_general_error(e, "regenerate_enhanced_strategy_ai_analysis")
raise ContentPlanningErrorHandler.handle_general_error(e, "regenerate_enhanced_strategy_ai_analysis")
@router.post("/{strategy_id}/autofill/accept")
async def accept_autofill_inputs(
strategy_id: int,
payload: Dict[str, Any],
db: Session = Depends(get_db)
) -> Dict[str, Any]:
"""Persist end-user accepted auto-fill inputs and associate with the strategy."""
try:
logger.info(f"🚀 Accepting autofill inputs for strategy: {strategy_id}")
user_id = int(payload.get('user_id') or 1)
accepted_fields = payload.get('accepted_fields') or {}
# Optional transparency bundles
sources = payload.get('sources') or {}
input_data_points = payload.get('input_data_points') or {}
quality_scores = payload.get('quality_scores') or {}
confidence_levels = payload.get('confidence_levels') or {}
data_freshness = payload.get('data_freshness') or {}
if not accepted_fields:
raise HTTPException(status_code=400, detail="accepted_fields is required")
db_service = EnhancedStrategyDBService(db)
record = await db_service.save_autofill_insights(
strategy_id=strategy_id,
user_id=user_id,
payload={
'accepted_fields': accepted_fields,
'sources': sources,
'input_data_points': input_data_points,
'quality_scores': quality_scores,
'confidence_levels': confidence_levels,
'data_freshness': data_freshness,
}
)
if not record:
raise HTTPException(status_code=500, detail="Failed to persist autofill insights")
return ResponseBuilder.create_success_response(
message="Accepted autofill inputs persisted successfully",
data={
'id': record.id,
'strategy_id': record.strategy_id,
'user_id': record.user_id,
'created_at': record.created_at.isoformat() if getattr(record, 'created_at', None) else None
}
)
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Error accepting autofill inputs: {str(e)}")
raise ContentPlanningErrorHandler.handle_general_error(e, "accept_autofill_inputs")
@router.get("/autofill/refresh/stream")
async def stream_autofill_refresh(
user_id: Optional[int] = Query(None, description="User ID to build auto-fill for"),
use_ai: bool = Query(True, description="Use AI augmentation during refresh"),
ai_only: bool = Query(False, description="AI-first refresh: return AI overrides when available"),
db: Session = Depends(get_db)
):
"""SSE endpoint to stream steps while generating a fresh auto-fill payload (no DB writes)."""
async def refresh_generator():
try:
actual_user_id = user_id or 1
start_time = datetime.utcnow()
logger.info(f"🚀 Starting auto-fill refresh stream for user: {actual_user_id}")
yield {"type": "status", "phase": "init", "message": "Starting…", "progress": 5}
refresh_service = AutoFillRefreshService(db)
# Phase: Collect onboarding context
yield {"type": "progress", "phase": "context", "message": "Collecting context…", "progress": 15}
# We deliberately do not emit DB-derived values; context is used inside the service
# Phase: Build prompt
yield {"type": "progress", "phase": "prompt", "message": "Preparing prompt…", "progress": 30}
# Phase: AI call - run in background and heartbeat until completion
yield {"type": "progress", "phase": "ai", "message": "Calling AI…", "progress": 45}
import asyncio
ai_task = asyncio.create_task(
refresh_service.build_fresh_payload(actual_user_id, use_ai=use_ai, ai_only=ai_only)
)
# Heartbeat loop while AI is running
heartbeat_progress = 50
while not ai_task.done():
elapsed = (datetime.utcnow() - start_time).total_seconds()
heartbeat_progress = min(heartbeat_progress + 3, 85)
yield {"type": "progress", "phase": "ai_running", "message": f"AI running… {int(elapsed)}s", "progress": heartbeat_progress}
await asyncio.sleep(2)
# Retrieve result or error
final_payload = await ai_task
# Phase: Validate & map
yield {"type": "progress", "phase": "validate", "message": "Validating…", "progress": 92}
# Phase: Transparency
yield {"type": "progress", "phase": "finalize", "message": "Finalizing…", "progress": 96}
total_ms = int((datetime.utcnow() - start_time).total_seconds() * 1000)
meta = final_payload.get('meta') or {}
meta.update({
'sse_total_ms': total_ms,
'sse_started_at': start_time.isoformat()
})
final_payload['meta'] = meta
yield {"type": "result", "status": "success", "data": final_payload, "progress": 100}
logger.info(f"✅ Auto-fill refresh stream completed for user: {actual_user_id} in {total_ms} ms")
except Exception as e:
logger.error(f"❌ Error in auto-fill refresh stream: {str(e)}")
yield {"type": "error", "message": str(e), "timestamp": datetime.utcnow().isoformat()}
return StreamingResponse(
stream_data(refresh_generator()),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Credentials": "true"
}
)
@router.post("/autofill/refresh")
async def refresh_autofill(
user_id: Optional[int] = Query(None, description="User ID to build auto-fill for"),
use_ai: bool = Query(True, description="Use AI augmentation during refresh"),
ai_only: bool = Query(False, description="AI-first refresh: return AI overrides when available"),
db: Session = Depends(get_db)
) -> Dict[str, Any]:
"""Non-stream endpoint to return a fresh auto-fill payload (no DB writes)."""
try:
actual_user_id = user_id or 1
started = datetime.utcnow()
refresh_service = AutoFillRefreshService(db)
payload = await refresh_service.build_fresh_payload(actual_user_id, use_ai=use_ai, ai_only=ai_only)
total_ms = int((datetime.utcnow() - started).total_seconds() * 1000)
meta = payload.get('meta') or {}
meta.update({'http_total_ms': total_ms, 'http_started_at': started.isoformat()})
payload['meta'] = meta
return ResponseBuilder.create_success_response(
message="Fresh auto-fill payload generated successfully",
data=payload
)
except Exception as e:
logger.error(f"❌ Error generating fresh auto-fill payload: {str(e)}")
raise ContentPlanningErrorHandler.handle_general_error(e, "refresh_autofill")