Recovered state: integrated TrendSurferAgent, restored frontend/backend files, and cleaned up recovery scripts
This commit is contained in:
1067
backend/api/agents_api.py
Normal file
1067
backend/api/agents_api.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@ from models.blog_models import (
|
||||
)
|
||||
from services.blog_writer.blog_service import BlogWriterService
|
||||
from services.blog_writer.seo.blog_seo_recommendation_applier import BlogSEORecommendationApplier
|
||||
from services.llm_providers.main_text_generation import llm_text_gen
|
||||
from .task_manager import task_manager
|
||||
from .cache_manager import cache_manager
|
||||
from models.blog_models import MediumBlogGenerateRequest
|
||||
@@ -97,6 +98,217 @@ async def apply_seo_recommendations(
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
class BlogSectionToolRequest(BaseModel):
|
||||
section_id: str = Field(..., description="Section id in blog writer UI")
|
||||
title: Optional[str] = Field(default=None, description="Section title/heading")
|
||||
content: str = Field(..., description="Section content text")
|
||||
keywords: List[str] = Field(default_factory=list, description="Optional target keywords")
|
||||
goal: Optional[str] = Field(default=None, description="Optional optimization goal")
|
||||
|
||||
|
||||
@router.post("/section/tools/originality")
|
||||
async def section_originality_tools(
|
||||
request: BlogSectionToolRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
if not current_user:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
user_id = str(current_user.get("id"))
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User ID not found in authentication token")
|
||||
|
||||
from services.intelligence.sif_integration import SIFIntegrationService
|
||||
from services.intelligence.sif_agents import ContentGuardianAgent
|
||||
|
||||
sif_service = SIFIntegrationService(user_id)
|
||||
intelligence = sif_service.intelligence_service
|
||||
|
||||
content = (request.content or "").strip()
|
||||
if len(content) < 50:
|
||||
return {
|
||||
"success": False,
|
||||
"section_id": request.section_id,
|
||||
"error": "Content too short for originality check",
|
||||
"matches": [],
|
||||
}
|
||||
|
||||
matches = await intelligence.search(content, limit=5)
|
||||
normalized_matches = []
|
||||
for m in matches or []:
|
||||
normalized_matches.append(
|
||||
{
|
||||
"id": m.get("id"),
|
||||
"score": m.get("score", 0.0),
|
||||
"excerpt": (m.get("text", "") or "")[:240],
|
||||
}
|
||||
)
|
||||
|
||||
guardian = ContentGuardianAgent(intelligence, sif_service=sif_service)
|
||||
cannibalization = await guardian.check_cannibalization(content)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"section_id": request.section_id,
|
||||
"cannibalization": cannibalization,
|
||||
"matches": normalized_matches,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to run originality tools: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/section/tools/internal-links")
|
||||
async def section_internal_link_tools(
|
||||
request: BlogSectionToolRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
if not current_user:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
user_id = str(current_user.get("id"))
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User ID not found in authentication token")
|
||||
|
||||
from services.intelligence.sif_integration import SIFIntegrationService
|
||||
from services.intelligence.sif_agents import LinkGraphAgent
|
||||
|
||||
sif_service = SIFIntegrationService(user_id)
|
||||
intelligence = sif_service.intelligence_service
|
||||
|
||||
content = (request.content or "").strip()
|
||||
suggestions = []
|
||||
if len(content) >= 50:
|
||||
link_agent = LinkGraphAgent(intelligence, sif_service=sif_service)
|
||||
suggestions = await link_agent.link_suggester(content)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"section_id": request.section_id,
|
||||
"suggestions": suggestions or [],
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to run internal link tools: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/section/tools/fact-check")
|
||||
async def section_fact_check_tools(
|
||||
request: BlogSectionToolRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
if not current_user:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
user_id = str(current_user.get("id"))
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User ID not found in authentication token")
|
||||
|
||||
from services.intelligence.sif_integration import SIFIntegrationService
|
||||
from services.intelligence.sif_agents import CitationExpert
|
||||
|
||||
sif_service = SIFIntegrationService(user_id)
|
||||
intelligence = sif_service.intelligence_service
|
||||
|
||||
expert = CitationExpert(intelligence)
|
||||
content = (request.content or "").strip()
|
||||
|
||||
verification = await expert.claim_verifier(content)
|
||||
|
||||
topic = request.title or content[:120]
|
||||
citations = await expert.citation_finder(topic)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"section_id": request.section_id,
|
||||
"verification": verification,
|
||||
"citations": citations or [],
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to run fact check tools: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/section/tools/optimize")
|
||||
async def section_optimize_tools(
|
||||
request: BlogSectionToolRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
if not current_user:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
user_id = str(current_user.get("id"))
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User ID not found in authentication token")
|
||||
|
||||
content = (request.content or "").strip()
|
||||
if len(content) < 50:
|
||||
return {
|
||||
"success": False,
|
||||
"section_id": request.section_id,
|
||||
"error": "Content too short for optimization",
|
||||
}
|
||||
|
||||
goal = request.goal or "readability"
|
||||
keywords_str = ", ".join(request.keywords or [])
|
||||
|
||||
system_prompt = (
|
||||
"You are an expert editor. Optimize the provided blog section while preserving meaning and tone."
|
||||
)
|
||||
prompt = (
|
||||
f"Optimization goal: {goal}\n"
|
||||
f"Target keywords (if any): {keywords_str}\n"
|
||||
f"Section title: {request.title or ''}\n\n"
|
||||
"Return a JSON object with keys:\n"
|
||||
'- optimized_content: string\n'
|
||||
'- changes_made: array of strings\n'
|
||||
"- diff_summary: string\n\n"
|
||||
f"Section content:\n{content}\n"
|
||||
)
|
||||
|
||||
json_struct = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"optimized_content": {"type": "string"},
|
||||
"changes_made": {"type": "array", "items": {"type": "string"}},
|
||||
"diff_summary": {"type": "string"},
|
||||
},
|
||||
"required": ["optimized_content", "changes_made", "diff_summary"],
|
||||
}
|
||||
|
||||
raw = llm_text_gen(prompt=prompt, system_prompt=system_prompt, json_struct=json_struct, user_id=user_id)
|
||||
data = None
|
||||
try:
|
||||
import json as _json
|
||||
|
||||
data = _json.loads(raw) if isinstance(raw, str) else raw
|
||||
except Exception:
|
||||
data = {
|
||||
"optimized_content": raw,
|
||||
"changes_made": ["Optimization applied"],
|
||||
"diff_summary": "Generated optimized version",
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"section_id": request.section_id,
|
||||
"optimized_content": data.get("optimized_content"),
|
||||
"changes_made": data.get("changes_made", []),
|
||||
"diff_summary": data.get("diff_summary"),
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to run optimize tools: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health() -> Dict[str, Any]:
|
||||
@@ -286,7 +498,8 @@ async def generate_section(
|
||||
) -> BlogSectionResponse:
|
||||
"""Generate content for a specific section."""
|
||||
try:
|
||||
response = await service.generate_section(request)
|
||||
user_id = str(current_user.get('id', '')) if current_user else None
|
||||
response = await service.generate_section(request, user_id=user_id)
|
||||
|
||||
# Save and track text content (non-blocking)
|
||||
if response.markdown:
|
||||
@@ -981,4 +1194,4 @@ async def generate_introductions(
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate introductions: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -10,10 +10,14 @@ from pydantic import BaseModel
|
||||
from typing import Dict, Any, Optional
|
||||
from loguru import logger
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
|
||||
from services.blog_writer.seo.blog_content_seo_analyzer import BlogContentSEOAnalyzer
|
||||
from services.blog_writer.core.blog_writer_service import BlogWriterService
|
||||
from middleware.auth_middleware import get_current_user
|
||||
from services.database import get_db
|
||||
from models.seo_analysis import SEOAnalysis
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/blog-writer/seo", tags=["Blog SEO Analysis"])
|
||||
@@ -147,7 +151,8 @@ async def analyze_blog_seo(
|
||||
@router.post("/analyze-with-progress")
|
||||
async def analyze_blog_seo_with_progress(
|
||||
request: SEOAnalysisRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Analyze blog content for SEO with real-time progress updates
|
||||
@@ -158,6 +163,7 @@ async def analyze_blog_seo_with_progress(
|
||||
Args:
|
||||
request: SEOAnalysisRequest containing blog content and research data
|
||||
current_user: Authenticated user from middleware
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Generator yielding progress updates and final results
|
||||
@@ -240,6 +246,35 @@ async def analyze_blog_seo_with_progress(
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
# Save to Database
|
||||
try:
|
||||
draft_url = f"draft:{analysis_id}"
|
||||
overall_score = analysis_results.get('overall_score', 0)
|
||||
|
||||
# Determine health status
|
||||
if overall_score >= 90:
|
||||
health_status = "excellent"
|
||||
elif overall_score >= 70:
|
||||
health_status = "good"
|
||||
elif overall_score >= 50:
|
||||
health_status = "needs_improvement"
|
||||
else:
|
||||
health_status = "poor"
|
||||
|
||||
new_analysis = SEOAnalysis(
|
||||
url=draft_url,
|
||||
overall_score=int(overall_score),
|
||||
health_status=health_status,
|
||||
timestamp=datetime.utcnow(),
|
||||
analysis_data=analysis_results
|
||||
)
|
||||
db.add(new_analysis)
|
||||
db.commit()
|
||||
logger.info(f"Saved SEO analysis results to DB for ID: {analysis_id}")
|
||||
except Exception as db_error:
|
||||
logger.error(f"Failed to save analysis to DB: {db_error}")
|
||||
# Continue without failing
|
||||
|
||||
# Final result
|
||||
yield SEOAnalysisProgress(
|
||||
analysis_id=analysis_id,
|
||||
@@ -273,27 +308,46 @@ async def analyze_blog_seo_with_progress(
|
||||
|
||||
|
||||
@router.get("/analysis/{analysis_id}")
|
||||
async def get_analysis_result(analysis_id: str):
|
||||
async def get_analysis_result(
|
||||
analysis_id: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get SEO analysis result by ID
|
||||
|
||||
Args:
|
||||
analysis_id: Unique identifier for the analysis
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
SEO analysis results
|
||||
"""
|
||||
try:
|
||||
# In a real implementation, you would store results in a database
|
||||
# For now, we'll return a placeholder
|
||||
logger.info(f"Retrieving SEO analysis result for ID: {analysis_id}")
|
||||
|
||||
return {
|
||||
"analysis_id": analysis_id,
|
||||
"status": "completed",
|
||||
"message": "Analysis results retrieved successfully"
|
||||
}
|
||||
# Look for the analysis in the database
|
||||
draft_url = f"draft:{analysis_id}"
|
||||
stmt = select(SEOAnalysis).where(SEOAnalysis.url == draft_url)
|
||||
analysis = db.execute(stmt).scalar_one_or_none()
|
||||
|
||||
if analysis and analysis.analysis_data:
|
||||
# Return stored analysis data
|
||||
return {
|
||||
"analysis_id": analysis_id,
|
||||
"status": "completed",
|
||||
"message": "Analysis results retrieved successfully",
|
||||
**analysis.analysis_data
|
||||
}
|
||||
|
||||
# If not found in DB (fallback for legacy or in-memory only)
|
||||
# For now, we return 404 to encourage DB usage, or we could return a placeholder if strictly needed.
|
||||
# But user requested DB integration, so we should rely on DB.
|
||||
|
||||
logger.warning(f"Analysis result not found in DB for ID: {analysis_id}")
|
||||
raise HTTPException(status_code=404, detail="Analysis result not found")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Get analysis result error: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to retrieve analysis result: {str(e)}")
|
||||
|
||||
@@ -12,6 +12,8 @@ from datetime import datetime
|
||||
from typing import Any, Dict, List
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
from sqlalchemy.orm import Session
|
||||
from services.database import SessionLocal, get_session_for_user
|
||||
|
||||
from models.blog_models import (
|
||||
BlogResearchRequest,
|
||||
@@ -261,11 +263,17 @@ class TaskManager:
|
||||
if total_target > 1000:
|
||||
raise ValueError("Global target words exceed 1000; medium generation not allowed")
|
||||
|
||||
result: MediumBlogGenerateResult = await self.service.generate_medium_blog_with_progress(
|
||||
request,
|
||||
task_id,
|
||||
user_id
|
||||
)
|
||||
# Create a sync session for asset saving
|
||||
db_session = SessionLocal()
|
||||
try:
|
||||
result: MediumBlogGenerateResult = await self.service.generate_medium_blog_with_progress(
|
||||
request,
|
||||
task_id,
|
||||
user_id,
|
||||
db=db_session
|
||||
)
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
if not result or not getattr(result, "sections", None):
|
||||
raise ValueError("Empty generation result from model")
|
||||
|
||||
@@ -31,13 +31,13 @@ from services.component_logic.style_detection_logic import StyleDetectionLogic
|
||||
from services.component_logic.web_crawler_logic import WebCrawlerLogic
|
||||
from services.research_preferences_service import ResearchPreferencesService
|
||||
from services.database import get_db
|
||||
from services.onboarding import OnboardingDatabaseService
|
||||
|
||||
# Import authentication for user isolation
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
# Import the website analysis service
|
||||
from services.website_analysis_service import WebsiteAnalysisService
|
||||
from services.seo_tools.sitemap_service import SitemapService
|
||||
from services.database import get_db_session
|
||||
|
||||
# Initialize services
|
||||
@@ -67,12 +67,33 @@ def clerk_user_id_to_int(user_id: str) -> int:
|
||||
|
||||
|
||||
def _get_onboarding_session(db_session: Session, user_id: str, create_if_missing: bool = False) -> Optional[OnboardingSession]:
|
||||
"""Fetch onboarding session for a user, optionally creating one."""
|
||||
db_service = OnboardingDatabaseService(db_session)
|
||||
session = db_service.get_session_by_user(user_id, db_session)
|
||||
if not session and create_if_missing:
|
||||
session = db_service.get_or_create_session(user_id, db_session)
|
||||
return session
|
||||
"""Fetch onboarding session for a user, optionally creating one.
|
||||
Refactored to use direct DB access instead of legacy OnboardingDatabaseService.
|
||||
"""
|
||||
try:
|
||||
session = db_session.query(OnboardingSession).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not session and create_if_missing:
|
||||
logger.info(f"Creating new onboarding session for user {user_id}")
|
||||
session = OnboardingSession(
|
||||
user_id=user_id,
|
||||
current_step=1,
|
||||
progress=0.0,
|
||||
started_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
db_session.add(session)
|
||||
db_session.commit()
|
||||
db_session.refresh(session)
|
||||
|
||||
return session
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting/creating onboarding session: {e}")
|
||||
if create_if_missing:
|
||||
db_session.rollback()
|
||||
return None
|
||||
|
||||
# AI Research Endpoints
|
||||
|
||||
@@ -218,8 +239,12 @@ async def validate_content_style(request: ContentStyleRequest):
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in validate_content_style: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
logger.error(f"Error in validate_content_style: {str(e)}", exc_info=True)
|
||||
return ContentStyleResponse(
|
||||
valid=False,
|
||||
style_config=None,
|
||||
errors=[f"Internal error validating content style: {str(e)}"]
|
||||
)
|
||||
|
||||
@router.post("/personalization/configure-brand", response_model=BrandVoiceResponse)
|
||||
async def configure_brand_voice(request: BrandVoiceRequest):
|
||||
@@ -242,8 +267,12 @@ async def configure_brand_voice(request: BrandVoiceRequest):
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in configure_brand_voice: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
logger.error(f"Error in configure_brand_voice: {str(e)}", exc_info=True)
|
||||
return BrandVoiceResponse(
|
||||
valid=False,
|
||||
brand_config=None,
|
||||
errors=[f"Internal error configuring brand voice: {str(e)}"]
|
||||
)
|
||||
|
||||
@router.post("/personalization/process-settings", response_model=PersonalizationSettingsResponse)
|
||||
async def process_personalization_settings(request: PersonalizationSettingsRequest):
|
||||
@@ -278,8 +307,12 @@ async def process_personalization_settings(request: PersonalizationSettingsReque
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in process_personalization_settings: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
logger.error(f"Error in process_personalization_settings: {str(e)}", exc_info=True)
|
||||
return PersonalizationSettingsResponse(
|
||||
valid=False,
|
||||
settings=None,
|
||||
errors=[f"Internal error processing settings: {str(e)}"]
|
||||
)
|
||||
|
||||
@router.get("/personalization/configuration-options")
|
||||
async def get_personalization_configuration_options():
|
||||
@@ -295,8 +328,21 @@ async def get_personalization_configuration_options():
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_personalization_configuration_options: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
logger.error(f"Error in get_personalization_configuration_options: {str(e)}", exc_info=True)
|
||||
# Fallback to default options to prevent 500 error
|
||||
return {
|
||||
'success': False,
|
||||
'options': {
|
||||
'writing_styles': ["Professional", "Casual", "Technical", "Conversational", "Academic"],
|
||||
'tones': ["Formal", "Semi-Formal", "Neutral", "Friendly", "Humorous"],
|
||||
'content_lengths': ["Concise", "Standard", "Detailed", "Comprehensive"],
|
||||
'personality_traits': ["Professional", "Innovative", "Friendly", "Trustworthy", "Creative", "Expert"],
|
||||
'readability_levels': ["Simple", "Standard", "Advanced", "Expert"],
|
||||
'content_structures': ["Introduction", "Key Points", "Examples", "Conclusion", "Call-to-Action"],
|
||||
'seo_optimization_options': [True, False]
|
||||
},
|
||||
'message': f"Error loading options: {str(e)}"
|
||||
}
|
||||
|
||||
@router.post("/personalization/generate-guidelines")
|
||||
async def generate_content_guidelines(settings: Dict[str, Any]):
|
||||
@@ -395,10 +441,14 @@ async def generate_research_report(results: Dict[str, Any]):
|
||||
|
||||
# Style Detection Endpoints
|
||||
@router.post("/style-detection/analyze", response_model=StyleAnalysisResponse)
|
||||
async def analyze_content_style(request: StyleAnalysisRequest):
|
||||
async def analyze_content_style(
|
||||
request: StyleAnalysisRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Analyze content style using AI."""
|
||||
try:
|
||||
logger.info("[analyze_content_style] Starting style analysis")
|
||||
user_id = str(current_user.get('id'))
|
||||
logger.info(f"[analyze_content_style] Starting style analysis for user: {user_id}")
|
||||
|
||||
# Initialize style detection logic
|
||||
style_logic = StyleDetectionLogic()
|
||||
@@ -414,9 +464,9 @@ async def analyze_content_style(request: StyleAnalysisRequest):
|
||||
|
||||
# Perform style analysis
|
||||
if request.analysis_type == "comprehensive":
|
||||
result = style_logic.analyze_content_style(validation['content'])
|
||||
result = style_logic.analyze_content_style(validation['content'], user_id=user_id)
|
||||
elif request.analysis_type == "patterns":
|
||||
result = style_logic.analyze_style_patterns(validation['content'])
|
||||
result = style_logic.analyze_style_patterns(validation['content'], user_id=user_id)
|
||||
else:
|
||||
return StyleAnalysisResponse(
|
||||
success=False,
|
||||
@@ -515,7 +565,7 @@ async def complete_style_detection(
|
||||
logger.info(f"[complete_style_detection] Starting complete style detection for user: {user_id}")
|
||||
|
||||
# Get database session
|
||||
db_session = get_db_session()
|
||||
db_session = get_db_session(user_id)
|
||||
if not db_session:
|
||||
return StyleDetectionResponse(
|
||||
success=False,
|
||||
@@ -527,6 +577,7 @@ async def complete_style_detection(
|
||||
crawler_logic = WebCrawlerLogic()
|
||||
style_logic = StyleDetectionLogic()
|
||||
analysis_service = WebsiteAnalysisService(db_session)
|
||||
sitemap_service = SitemapService()
|
||||
|
||||
session = _get_onboarding_session(db_session, user_id, create_if_missing=True)
|
||||
if not session:
|
||||
@@ -573,19 +624,49 @@ async def complete_style_detection(
|
||||
async def run_style_analysis():
|
||||
"""Run style analysis in executor"""
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(None, partial(style_logic.analyze_content_style, crawl_result['content']))
|
||||
return await loop.run_in_executor(None, partial(style_logic.analyze_content_style, crawl_result['content'], user_id=user_id))
|
||||
|
||||
async def run_patterns_analysis():
|
||||
"""Run patterns analysis in executor (if requested)"""
|
||||
if not request.include_patterns:
|
||||
return None
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(None, partial(style_logic.analyze_style_patterns, crawl_result['content']))
|
||||
return await loop.run_in_executor(None, partial(style_logic.analyze_style_patterns, crawl_result['content'], user_id=user_id))
|
||||
|
||||
# Execute style and patterns analysis in parallel
|
||||
style_analysis, patterns_result = await asyncio.gather(
|
||||
async def run_seo_audit():
|
||||
"""Run SEO audit in executor"""
|
||||
if not request.url:
|
||||
return None
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(None, partial(style_logic.perform_seo_audit, request.url, crawl_result['content']))
|
||||
|
||||
async def run_sitemap_analysis():
|
||||
"""Run AI sitemap analysis for home page"""
|
||||
if not request.url:
|
||||
return None
|
||||
try:
|
||||
# Discover sitemap URL
|
||||
sitemap_url = await sitemap_service.discover_sitemap_url(request.url)
|
||||
if sitemap_url:
|
||||
# Analyze sitemap with AI insights
|
||||
return await sitemap_service.analyze_sitemap(
|
||||
sitemap_url=sitemap_url,
|
||||
analyze_content_trends=True,
|
||||
analyze_publishing_patterns=True,
|
||||
include_ai_insights=True,
|
||||
user_id=user_id
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Sitemap analysis failed: {e}")
|
||||
return None
|
||||
|
||||
# Execute style, patterns, SEO analysis and sitemap analysis in parallel
|
||||
style_analysis, patterns_result, seo_audit_result, sitemap_result = await asyncio.gather(
|
||||
run_style_analysis(),
|
||||
run_patterns_analysis(),
|
||||
run_seo_audit(),
|
||||
run_sitemap_analysis(),
|
||||
return_exceptions=True
|
||||
)
|
||||
|
||||
@@ -622,13 +703,27 @@ async def complete_style_detection(
|
||||
if patterns_result.get('success'):
|
||||
style_patterns = patterns_result.get('patterns')
|
||||
|
||||
# Process SEO audit result
|
||||
seo_audit = None
|
||||
if seo_audit_result and not isinstance(seo_audit_result, Exception):
|
||||
seo_audit = seo_audit_result
|
||||
elif isinstance(seo_audit_result, Exception):
|
||||
logger.warning(f"SEO audit failed: {seo_audit_result}")
|
||||
|
||||
# Process sitemap analysis result
|
||||
sitemap_analysis = None
|
||||
if sitemap_result and not isinstance(sitemap_result, Exception):
|
||||
sitemap_analysis = sitemap_result
|
||||
elif isinstance(sitemap_result, Exception):
|
||||
logger.warning(f"Sitemap analysis failed: {sitemap_result}")
|
||||
|
||||
# Step 4: Generate guidelines (depends on style_analysis, must run after)
|
||||
style_guidelines = None
|
||||
if request.include_guidelines:
|
||||
loop = asyncio.get_event_loop()
|
||||
guidelines_result = await loop.run_in_executor(
|
||||
None,
|
||||
partial(style_logic.generate_style_guidelines, style_analysis.get('analysis', {}))
|
||||
partial(style_logic.generate_style_guidelines, style_analysis.get('analysis', {}), user_id=user_id)
|
||||
)
|
||||
if guidelines_result and guidelines_result.get('success'):
|
||||
style_guidelines = guidelines_result.get('guidelines')
|
||||
@@ -644,6 +739,8 @@ async def complete_style_detection(
|
||||
'style_analysis': style_analysis.get('analysis') if style_analysis else None,
|
||||
'style_patterns': style_patterns,
|
||||
'style_guidelines': style_guidelines,
|
||||
'seo_audit': seo_audit,
|
||||
'sitemap_analysis': sitemap_analysis,
|
||||
'warning': warning
|
||||
}
|
||||
|
||||
@@ -659,6 +756,8 @@ async def complete_style_detection(
|
||||
style_analysis=style_analysis.get('analysis') if style_analysis else None,
|
||||
style_patterns=style_patterns,
|
||||
style_guidelines=style_guidelines,
|
||||
seo_audit=seo_audit,
|
||||
sitemap_analysis=sitemap_analysis,
|
||||
warning=warning,
|
||||
timestamp=datetime.now().isoformat()
|
||||
)
|
||||
@@ -682,19 +781,20 @@ async def check_existing_analysis(
|
||||
logger.info(f"[check_existing_analysis] Checking for URL: {website_url} (user: {user_id})")
|
||||
|
||||
# Get database session
|
||||
db_session = get_db_session()
|
||||
db_session = get_db_session(user_id)
|
||||
if not db_session:
|
||||
return {"error": "Database connection not available"}
|
||||
|
||||
# Initialize service
|
||||
analysis_service = WebsiteAnalysisService(db_session)
|
||||
|
||||
# Use authenticated Clerk user ID for proper user isolation
|
||||
# Use consistent SHA256-based conversion
|
||||
user_id_int = clerk_user_id_to_int(user_id)
|
||||
# Get onboarding session to ensure we check the correct session
|
||||
session = _get_onboarding_session(db_session, user_id)
|
||||
if not session:
|
||||
return {'exists': False}
|
||||
|
||||
# Check for existing analysis for THIS USER ONLY
|
||||
existing_analysis = analysis_service.check_existing_analysis(user_id_int, website_url)
|
||||
# Check for existing analysis for THIS USER'S SESSION
|
||||
existing_analysis = analysis_service.check_existing_analysis(session.id, website_url)
|
||||
|
||||
return existing_analysis
|
||||
|
||||
@@ -703,23 +803,33 @@ async def check_existing_analysis(
|
||||
return {"error": f"Error checking existing analysis: {str(e)}"}
|
||||
|
||||
@router.get("/style-detection/analysis/{analysis_id}")
|
||||
async def get_analysis_by_id(analysis_id: int):
|
||||
async def get_analysis_by_id(
|
||||
analysis_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Get analysis by ID."""
|
||||
try:
|
||||
logger.info(f"[get_analysis_by_id] Getting analysis: {analysis_id}")
|
||||
user_id = str(current_user.get('id'))
|
||||
logger.info(f"[get_analysis_by_id] Getting analysis: {analysis_id} (user: {user_id})")
|
||||
|
||||
# Get database session
|
||||
db_session = get_db_session()
|
||||
db_session = get_db_session(user_id)
|
||||
if not db_session:
|
||||
return {"error": "Database connection not available"}
|
||||
|
||||
# Initialize service
|
||||
analysis_service = WebsiteAnalysisService(db_session)
|
||||
|
||||
# Get onboarding session to ensure ownership
|
||||
session = _get_onboarding_session(db_session, user_id)
|
||||
if not session:
|
||||
return {"success": False, "error": "Analysis not found"}
|
||||
|
||||
# Get analysis
|
||||
analysis = analysis_service.get_analysis(analysis_id)
|
||||
|
||||
if analysis:
|
||||
# Verify ownership (session_id must match)
|
||||
if analysis and analysis.get('session_id') == session.id:
|
||||
return {"success": True, "analysis": analysis}
|
||||
else:
|
||||
return {"success": False, "error": "Analysis not found"}
|
||||
@@ -733,22 +843,23 @@ async def get_session_analyses(current_user: Dict[str, Any] = Depends(get_curren
|
||||
"""Get all analyses for the current user with proper user isolation."""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
logger.info(f"[get_session_analyses] Getting analyses for user: {user_id}")
|
||||
logger.info(f"[get_session_analyses] Getting analyses for user: {user_id})")
|
||||
|
||||
# Get database session
|
||||
db_session = get_db_session()
|
||||
db_session = get_db_session(user_id)
|
||||
if not db_session:
|
||||
return {"error": "Database connection not available"}
|
||||
|
||||
# Initialize service
|
||||
analysis_service = WebsiteAnalysisService(db_session)
|
||||
|
||||
# Use authenticated Clerk user ID for proper user isolation
|
||||
# Use consistent SHA256-based conversion
|
||||
user_id_int = clerk_user_id_to_int(user_id)
|
||||
# Get onboarding session to ensure we fetch analyses for the correct session
|
||||
session = _get_onboarding_session(db_session, user_id)
|
||||
if not session:
|
||||
return {"success": True, "analyses": []}
|
||||
|
||||
# Get analyses for THIS USER ONLY (not all users!)
|
||||
analyses = analysis_service.get_session_analyses(user_id_int)
|
||||
# Get analyses for THIS USER'S SESSION
|
||||
analyses = analysis_service.get_session_analyses(session.id)
|
||||
|
||||
logger.info(f"[get_session_analyses] Found {len(analyses) if analyses else 0} analyses for user {user_id}")
|
||||
return {"success": True, "analyses": analyses}
|
||||
@@ -757,28 +868,107 @@ async def get_session_analyses(current_user: Dict[str, Any] = Depends(get_curren
|
||||
logger.error(f"[get_session_analyses] Error: {str(e)}")
|
||||
return {"error": f"Error retrieving session analyses: {str(e)}"}
|
||||
|
||||
@router.delete("/style-detection/analysis/{analysis_id}")
|
||||
async def delete_analysis(analysis_id: int):
|
||||
"""Delete an analysis."""
|
||||
@router.put("/style-detection/analysis/{analysis_id}")
|
||||
async def update_analysis(
|
||||
analysis_id: int,
|
||||
analysis_data: Dict[str, Any],
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Update an existing analysis with edited content."""
|
||||
try:
|
||||
logger.info(f"[delete_analysis] Deleting analysis: {analysis_id}")
|
||||
user_id = str(current_user.get('id'))
|
||||
logger.info(f"[update_analysis] Updating analysis: {analysis_id} (user: {user_id})")
|
||||
|
||||
# Get database session
|
||||
db_session = get_db_session()
|
||||
db_session = get_db_session(user_id)
|
||||
if not db_session:
|
||||
return {"error": "Database connection not available"}
|
||||
|
||||
# Initialize service
|
||||
analysis_service = WebsiteAnalysisService(db_session)
|
||||
|
||||
# Get onboarding session to ensure ownership
|
||||
session = _get_onboarding_session(db_session, user_id)
|
||||
if not session:
|
||||
return {"success": False, "error": "Analysis not found"}
|
||||
|
||||
# Check ownership first
|
||||
analysis = analysis_service.get_analysis(analysis_id)
|
||||
if not analysis or analysis.get('session_id') != session.id:
|
||||
return {"success": False, "error": "Analysis not found"}
|
||||
|
||||
# Update analysis
|
||||
# Reconstruct style_guidelines if individual fields are passed
|
||||
# The frontend flat structure: guidelines, best_practices, etc.
|
||||
# The DB structure: style_guidelines JSON
|
||||
|
||||
if any(k in analysis_data for k in ['guidelines', 'best_practices', 'avoid_elements', 'content_strategy', 'ai_generation_tips', 'competitive_advantages', 'content_calendar_suggestions']):
|
||||
# Fetch existing style_guidelines to merge or create new
|
||||
existing_guidelines = analysis.get('style_guidelines') or {}
|
||||
|
||||
mapping = {
|
||||
'guidelines': 'guidelines',
|
||||
'best_practices': 'best_practices',
|
||||
'avoid_elements': 'avoid_elements',
|
||||
'content_strategy': 'content_strategy',
|
||||
'ai_generation_tips': 'ai_generation_tips',
|
||||
'competitive_advantages': 'competitive_advantages',
|
||||
'content_calendar_suggestions': 'content_calendar_suggestions'
|
||||
}
|
||||
|
||||
for frontend_key, db_key in mapping.items():
|
||||
if frontend_key in analysis_data:
|
||||
existing_guidelines[db_key] = analysis_data[frontend_key]
|
||||
|
||||
analysis_data['style_guidelines'] = existing_guidelines
|
||||
|
||||
success = analysis_service.update_analysis_content(analysis_id, analysis_data)
|
||||
|
||||
if success:
|
||||
return {"success": True}
|
||||
else:
|
||||
return {"success": False, "error": "Failed to update analysis"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[update_analysis] Error: {str(e)}")
|
||||
return {"error": f"Error updating analysis: {str(e)}"}
|
||||
|
||||
@router.delete("/style-detection/analysis/{analysis_id}")
|
||||
async def delete_analysis(
|
||||
analysis_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Delete an analysis."""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
logger.info(f"[delete_analysis] Deleting analysis: {analysis_id} (user: {user_id})")
|
||||
|
||||
# Get database session
|
||||
db_session = get_db_session(user_id)
|
||||
if not db_session:
|
||||
return {"error": "Database connection not available"}
|
||||
|
||||
# Initialize service
|
||||
analysis_service = WebsiteAnalysisService(db_session)
|
||||
|
||||
# Get onboarding session to ensure ownership
|
||||
session = _get_onboarding_session(db_session, user_id)
|
||||
if not session:
|
||||
return {"success": False, "error": "Analysis not found"}
|
||||
|
||||
# Check ownership first
|
||||
analysis = analysis_service.get_analysis(analysis_id)
|
||||
if not analysis or analysis.get('session_id') != session.id:
|
||||
return {"success": False, "error": "Analysis not found"}
|
||||
|
||||
# Delete analysis
|
||||
success = analysis_service.delete_analysis(analysis_id)
|
||||
|
||||
if success:
|
||||
return {"success": True, "message": "Analysis deleted successfully"}
|
||||
return {"success": True}
|
||||
else:
|
||||
return {"success": False, "error": "Analysis not found or could not be deleted"}
|
||||
|
||||
return {"success": False, "error": "Failed to delete analysis"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[delete_analysis] Error: {str(e)}")
|
||||
return {"error": f"Error deleting analysis: {str(e)}"}
|
||||
|
||||
@@ -54,7 +54,7 @@ async def accept_autofill_inputs(
|
||||
"""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)
|
||||
user_id = str(payload.get('user_id') or "")
|
||||
accepted_fields = payload.get('accepted_fields') or {}
|
||||
# Optional transparency bundles
|
||||
sources = payload.get('sources') or {}
|
||||
@@ -224,4 +224,4 @@ async def refresh_autofill(
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error generating fresh auto-fill payload: {str(e)}")
|
||||
raise ContentPlanningErrorHandler.handle_general_error(e, "refresh_autofill")
|
||||
raise ContentPlanningErrorHandler.handle_general_error(e, "refresh_autofill")
|
||||
|
||||
@@ -11,7 +11,7 @@ import json
|
||||
from datetime import datetime
|
||||
|
||||
# Import database
|
||||
from services.database import get_db_session
|
||||
from services.database import get_db
|
||||
|
||||
# Import authentication middleware
|
||||
from middleware.auth_middleware import get_current_user
|
||||
@@ -31,13 +31,6 @@ from ....utils.data_parsers import parse_strategy_data
|
||||
|
||||
router = APIRouter(tags=["Strategy CRUD"])
|
||||
|
||||
# Helper function to get database session
|
||||
def get_db():
|
||||
db = get_db_session()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@router.post("/create")
|
||||
async def create_enhanced_strategy(
|
||||
@@ -104,7 +97,7 @@ async def create_enhanced_strategy(
|
||||
|
||||
@router.get("/")
|
||||
async def get_enhanced_strategies(
|
||||
user_id: Optional[int] = Query(None, description="User ID to filter strategies (deprecated - use authenticated user)"),
|
||||
user_id: Optional[str] = Query(None, description="User ID to filter strategies (deprecated - use authenticated user)"),
|
||||
strategy_id: Optional[int] = Query(None, description="Specific strategy ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
@@ -119,8 +112,7 @@ async def get_enhanced_strategies(
|
||||
detail="Invalid user ID in authentication token"
|
||||
)
|
||||
|
||||
# Use authenticated user_id (override query parameter for security)
|
||||
authenticated_user_id = int(clerk_user_id) if clerk_user_id.isdigit() else None
|
||||
authenticated_user_id = clerk_user_id
|
||||
|
||||
logger.info(f"Getting enhanced strategies for authenticated user: {authenticated_user_id}, strategy: {strategy_id}")
|
||||
|
||||
@@ -148,7 +140,6 @@ async def get_enhanced_strategy_by_id(
|
||||
) -> Dict[str, Any]:
|
||||
"""Get a specific enhanced strategy by ID."""
|
||||
try:
|
||||
# Extract authenticated user_id from Clerk
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
if not clerk_user_id:
|
||||
raise HTTPException(
|
||||
@@ -156,7 +147,7 @@ async def get_enhanced_strategy_by_id(
|
||||
detail="Invalid user ID in authentication token"
|
||||
)
|
||||
|
||||
authenticated_user_id = int(clerk_user_id) if clerk_user_id.isdigit() else None
|
||||
authenticated_user_id = clerk_user_id
|
||||
|
||||
logger.info(f"Getting enhanced strategy by ID: {strategy_id} for authenticated user: {authenticated_user_id}")
|
||||
|
||||
@@ -201,7 +192,6 @@ async def update_enhanced_strategy(
|
||||
) -> Dict[str, Any]:
|
||||
"""Update an enhanced strategy."""
|
||||
try:
|
||||
# Extract authenticated user_id from Clerk
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
if not clerk_user_id:
|
||||
raise HTTPException(
|
||||
@@ -209,7 +199,7 @@ async def update_enhanced_strategy(
|
||||
detail="Invalid user ID in authentication token"
|
||||
)
|
||||
|
||||
authenticated_user_id = int(clerk_user_id) if clerk_user_id.isdigit() else None
|
||||
authenticated_user_id = clerk_user_id
|
||||
|
||||
logger.info(f"Updating enhanced strategy: {strategy_id} for authenticated user: {authenticated_user_id}")
|
||||
|
||||
@@ -270,7 +260,7 @@ async def delete_enhanced_strategy(
|
||||
detail="Invalid user ID in authentication token"
|
||||
)
|
||||
|
||||
authenticated_user_id = int(clerk_user_id) if clerk_user_id.isdigit() else None
|
||||
authenticated_user_id = clerk_user_id
|
||||
|
||||
logger.info(f"Deleting enhanced strategy: {strategy_id} for authenticated user: {authenticated_user_id}")
|
||||
|
||||
@@ -306,4 +296,4 @@ async def delete_enhanced_strategy(
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting enhanced strategy: {str(e)}")
|
||||
return ContentPlanningErrorHandler.handle_general_error(e, "delete_enhanced_strategy")
|
||||
return ContentPlanningErrorHandler.handle_general_error(e, "delete_enhanced_strategy")
|
||||
|
||||
@@ -78,16 +78,12 @@ async def stream_enhanced_strategies(
|
||||
|
||||
async def strategy_generator():
|
||||
try:
|
||||
# Extract authenticated user_id from Clerk
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
if not clerk_user_id:
|
||||
yield {"type": "error", "message": "Invalid user ID in authentication token", "timestamp": datetime.utcnow().isoformat()}
|
||||
return
|
||||
|
||||
authenticated_user_id = int(clerk_user_id) if clerk_user_id.isdigit() else None
|
||||
if not authenticated_user_id:
|
||||
yield {"type": "error", "message": "Invalid user ID format", "timestamp": datetime.utcnow().isoformat()}
|
||||
return
|
||||
authenticated_user_id = clerk_user_id
|
||||
|
||||
logger.info(f"🚀 Starting strategy stream for authenticated user: {authenticated_user_id}, strategy: {strategy_id}")
|
||||
|
||||
@@ -145,16 +141,12 @@ async def stream_strategic_intelligence(
|
||||
|
||||
async def intelligence_generator():
|
||||
try:
|
||||
# Extract authenticated user_id from Clerk
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
if not clerk_user_id:
|
||||
yield {"type": "error", "message": "Invalid user ID in authentication token", "timestamp": datetime.utcnow().isoformat()}
|
||||
return
|
||||
|
||||
authenticated_user_id = int(clerk_user_id) if clerk_user_id.isdigit() else None
|
||||
if not authenticated_user_id:
|
||||
yield {"type": "error", "message": "Invalid user ID format", "timestamp": datetime.utcnow().isoformat()}
|
||||
return
|
||||
authenticated_user_id = clerk_user_id
|
||||
|
||||
logger.info(f"🚀 Starting strategic intelligence stream for authenticated user: {authenticated_user_id}")
|
||||
|
||||
@@ -286,16 +278,12 @@ async def stream_keyword_research(
|
||||
|
||||
async def keyword_generator():
|
||||
try:
|
||||
# Extract authenticated user_id from Clerk
|
||||
clerk_user_id = str(current_user.get('id', ''))
|
||||
if not clerk_user_id:
|
||||
yield {"type": "error", "message": "Invalid user ID in authentication token", "timestamp": datetime.utcnow().isoformat()}
|
||||
return
|
||||
|
||||
authenticated_user_id = int(clerk_user_id) if clerk_user_id.isdigit() else None
|
||||
if not authenticated_user_id:
|
||||
yield {"type": "error", "message": "Invalid user ID format", "timestamp": datetime.utcnow().isoformat()}
|
||||
return
|
||||
authenticated_user_id = clerk_user_id
|
||||
|
||||
logger.info(f"🚀 Starting keyword research stream for authenticated user: {authenticated_user_id}")
|
||||
|
||||
@@ -396,4 +384,4 @@ async def stream_keyword_research(
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Credentials": "true"
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ from ...utils.constants import ERROR_MESSAGES, SUCCESS_MESSAGES
|
||||
|
||||
# Import services
|
||||
from ...services.ai_analytics_service import ContentPlanningAIAnalyticsService
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
# Initialize services
|
||||
ai_analytics_service = ContentPlanningAIAnalyticsService()
|
||||
@@ -37,14 +38,19 @@ ai_analytics_service = ContentPlanningAIAnalyticsService()
|
||||
router = APIRouter(prefix="/ai-analytics", tags=["ai-analytics"])
|
||||
|
||||
@router.post("/content-evolution", response_model=AIAnalyticsResponse)
|
||||
async def analyze_content_evolution(request: ContentEvolutionRequest):
|
||||
async def analyze_content_evolution(
|
||||
request: ContentEvolutionRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Analyze content evolution over time for a specific strategy.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Starting content evolution analysis for strategy {request.strategy_id}")
|
||||
user_id = current_user.get("user_id")
|
||||
logger.info(f"Starting content evolution analysis for strategy {request.strategy_id} (user {user_id})")
|
||||
|
||||
result = await ai_analytics_service.analyze_content_evolution(
|
||||
user_id=user_id,
|
||||
strategy_id=request.strategy_id,
|
||||
time_period=request.time_period
|
||||
)
|
||||
@@ -103,14 +109,19 @@ async def predict_content_performance(request: ContentPerformancePredictionReque
|
||||
)
|
||||
|
||||
@router.post("/strategic-intelligence", response_model=AIAnalyticsResponse)
|
||||
async def generate_strategic_intelligence(request: StrategicIntelligenceRequest):
|
||||
async def generate_strategic_intelligence(
|
||||
request: StrategicIntelligenceRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Generate strategic intelligence for content planning.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Starting strategic intelligence generation for strategy {request.strategy_id}")
|
||||
user_id = current_user.get("user_id")
|
||||
logger.info(f"Starting strategic intelligence generation for strategy {request.strategy_id} (user {user_id})")
|
||||
|
||||
result = await ai_analytics_service.generate_strategic_intelligence(
|
||||
user_id=user_id,
|
||||
strategy_id=request.strategy_id,
|
||||
market_data=request.market_data
|
||||
)
|
||||
|
||||
@@ -10,6 +10,9 @@ from datetime import datetime
|
||||
from loguru import logger
|
||||
import json
|
||||
|
||||
# Import auth middleware
|
||||
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
|
||||
@@ -54,12 +57,13 @@ async def create_content_gap_analysis(
|
||||
|
||||
@router.get("/", response_model=Dict[str, Any])
|
||||
async def get_content_gap_analyses(
|
||||
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 gap analysis")
|
||||
force_refresh: bool = Query(False, description="Force refresh gap analysis"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Get content gap analysis with real AI insights - Database first approach."""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
logger.info(f"🚀 Starting content gap analysis for user: {user_id}, strategy: {strategy_id}, force_refresh: {force_refresh}")
|
||||
|
||||
result = await gap_analysis_service.get_gap_analyses(user_id, strategy_id, force_refresh)
|
||||
@@ -88,24 +92,27 @@ async def get_content_gap_analysis(
|
||||
raise ContentPlanningErrorHandler.handle_general_error(e, "get_content_gap_analysis")
|
||||
|
||||
@router.post("/analyze", response_model=ContentGapAnalysisFullResponse)
|
||||
async def analyze_content_gaps(request: ContentGapAnalysisRequest):
|
||||
async def analyze_content_gaps(
|
||||
request: ContentGapAnalysisRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Analyze content gaps between your website and competitors.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Starting content gap analysis for: {request.website_url}")
|
||||
|
||||
user_id = str(current_user.get('id'))
|
||||
request_data = request.dict()
|
||||
result = await gap_analysis_service.analyze_content_gaps(request_data)
|
||||
result = await gap_analysis_service.analyze_content_gaps(request_data, user_id)
|
||||
|
||||
return ContentGapAnalysisFullResponse(**result)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing content gaps: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Error analyzing content gaps: {str(e)}"
|
||||
)
|
||||
raise ContentPlanningErrorHandler.handle_general_error(e, "analyze_content_gaps")
|
||||
|
||||
@router.get("/user/{user_id}/analyses")
|
||||
async def get_user_gap_analyses(
|
||||
|
||||
@@ -3,21 +3,23 @@ API Monitoring Routes
|
||||
Simple endpoints to expose API monitoring and cache statistics.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from typing import Dict, Any
|
||||
from loguru import logger
|
||||
|
||||
from services.subscription import get_monitoring_stats, get_lightweight_stats
|
||||
from services.comprehensive_user_data_cache_service import ComprehensiveUserDataCacheService
|
||||
from services.database import get_db
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/monitoring", tags=["monitoring"])
|
||||
|
||||
@router.get("/api-stats")
|
||||
async def get_api_statistics(minutes: int = 5) -> Dict[str, Any]:
|
||||
async def get_api_statistics(minutes: int = 5, current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""Get current API monitoring statistics."""
|
||||
try:
|
||||
stats = await get_monitoring_stats(minutes)
|
||||
user_id = current_user.get('id') or current_user.get('clerk_user_id')
|
||||
stats = await get_monitoring_stats(minutes=minutes)
|
||||
return {
|
||||
"status": "success",
|
||||
"data": stats,
|
||||
@@ -28,18 +30,67 @@ async def get_api_statistics(minutes: int = 5) -> Dict[str, Any]:
|
||||
raise HTTPException(status_code=500, detail="Failed to get API statistics")
|
||||
|
||||
@router.get("/lightweight-stats")
|
||||
async def get_lightweight_statistics() -> Dict[str, Any]:
|
||||
async def get_lightweight_statistics(current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""Get lightweight stats for dashboard header."""
|
||||
try:
|
||||
stats = await get_lightweight_stats()
|
||||
logger.info(f"DEBUG: get_lightweight_statistics called. current_user type: {type(current_user)}")
|
||||
logger.info(f"DEBUG: current_user content: {current_user}")
|
||||
|
||||
user_id = current_user.get('id') or current_user.get('clerk_user_id')
|
||||
logger.info(f"Fetching lightweight stats for user: {user_id}")
|
||||
|
||||
if not user_id:
|
||||
logger.error(f"User ID is missing from current_user: {current_user}")
|
||||
# Return empty stats instead of 500
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"status": "unknown",
|
||||
"icon": "⚪",
|
||||
"recent_requests": 0,
|
||||
"recent_errors": 0,
|
||||
"error_rate": 0.0,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
},
|
||||
"message": "User ID missing, returning empty stats"
|
||||
}
|
||||
|
||||
try:
|
||||
stats = await get_lightweight_stats(user_id)
|
||||
logger.info(f"DEBUG: stats retrieved: {stats}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error calling get_lightweight_stats: {str(e)}", exc_info=True)
|
||||
# Return empty stats instead of 500 to keep frontend alive
|
||||
stats = {
|
||||
"status": "unknown",
|
||||
"icon": "⚪",
|
||||
"recent_requests": 0,
|
||||
"recent_errors": 0,
|
||||
"error_rate": 0.0,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": stats,
|
||||
"message": "Lightweight monitoring statistics retrieved successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting lightweight stats: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to get lightweight statistics")
|
||||
logger.error(f"Error getting lightweight stats: {str(e)}", exc_info=True)
|
||||
# Even top-level error should not 500 if possible, but at least we log it.
|
||||
# We'll return a safe response here too.
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"status": "error",
|
||||
"icon": "🔴",
|
||||
"recent_requests": 0,
|
||||
"recent_errors": 0,
|
||||
"error_rate": 0.0,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
},
|
||||
"message": f"Error retrieving stats: {str(e)}"
|
||||
}
|
||||
|
||||
@router.get("/cache-stats")
|
||||
async def get_cache_statistics(db = None) -> Dict[str, Any]:
|
||||
@@ -61,14 +112,15 @@ async def get_cache_statistics(db = None) -> Dict[str, Any]:
|
||||
raise HTTPException(status_code=500, detail="Failed to get cache statistics")
|
||||
|
||||
@router.get("/health")
|
||||
async def get_system_health() -> Dict[str, Any]:
|
||||
async def get_system_health(current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""Get overall system health status.
|
||||
|
||||
Optimized to fail fast - cache stats are optional and won't block the response.
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get('id') or current_user.get('clerk_user_id')
|
||||
# Get lightweight API stats (this is the critical path)
|
||||
api_stats = await get_lightweight_stats()
|
||||
api_stats = await get_lightweight_stats(user_id)
|
||||
|
||||
# Get cache stats if available (non-blocking - don't fail if unavailable)
|
||||
cache_stats = {}
|
||||
|
||||
@@ -9,8 +9,11 @@ from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
|
||||
# Import auth middleware
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
# Import database service
|
||||
from services.database import get_db_session, get_db
|
||||
from services.database import get_db, get_session_for_user
|
||||
from services.content_planning_db import ContentPlanningDBService
|
||||
|
||||
# Import models
|
||||
@@ -53,21 +56,37 @@ async def create_content_strategy(
|
||||
|
||||
@router.get("/", response_model=Dict[str, Any])
|
||||
async def get_content_strategies(
|
||||
user_id: Optional[int] = Query(None, description="User ID"),
|
||||
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)
|
||||
):
|
||||
"""
|
||||
Get content strategies with comprehensive logging for debugging.
|
||||
"""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
logger.info(f"🚀 Starting content strategy analysis for user: {user_id}, strategy: {strategy_id}")
|
||||
|
||||
# Create a temporary database session for this operation
|
||||
from services.database import get_db_session
|
||||
temp_db = get_db_session()
|
||||
temp_db = get_session_for_user(user_id)
|
||||
if not temp_db:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed")
|
||||
|
||||
try:
|
||||
db_service = EnhancedStrategyDBService(temp_db)
|
||||
strategy_service = EnhancedStrategyService(db_service)
|
||||
# Pass user_id (as int or str depending on service expectation)
|
||||
# EnhancedStrategyService.get_enhanced_strategies usually takes user_id but here it seems to filter by strategy_id
|
||||
# If user_id is needed for filtering by user, we should check the service signature.
|
||||
# But the service uses the DB session which is already filtered by user (SQLite isolation).
|
||||
# So passing user_id might be for logging or legacy filtering.
|
||||
|
||||
# Note: The original code passed user_id from query param.
|
||||
# We pass the authenticated user_id.
|
||||
# Assuming the service can handle string user_id or we convert to int if it expects int.
|
||||
# Most legacy IDs were ints. Clerk IDs are strings.
|
||||
# Let's try to convert to int if possible, or pass as is.
|
||||
# Since SQLite isolation is used, the DB only contains this user's data.
|
||||
|
||||
result = await strategy_service.get_enhanced_strategies(user_id, strategy_id, temp_db)
|
||||
return result
|
||||
finally:
|
||||
|
||||
@@ -13,7 +13,8 @@ import time
|
||||
from services.content_planning_db import ContentPlanningDBService
|
||||
from services.ai_analysis_db_service import AIAnalysisDBService
|
||||
from services.ai_analytics_service import AIAnalyticsService
|
||||
from services.onboarding.data_service import OnboardingDataService
|
||||
from services.database import SessionLocal
|
||||
from api.content_planning.services.content_strategy.onboarding import OnboardingDataIntegrationService
|
||||
|
||||
# Import utilities
|
||||
from ..utils.error_handlers import ContentPlanningErrorHandler
|
||||
@@ -26,15 +27,16 @@ class ContentPlanningAIAnalyticsService:
|
||||
def __init__(self):
|
||||
self.ai_analysis_db_service = AIAnalysisDBService()
|
||||
self.ai_analytics_service = AIAnalyticsService()
|
||||
self.onboarding_service = OnboardingDataService()
|
||||
self.onboarding_integration_service = OnboardingDataIntegrationService()
|
||||
|
||||
async def analyze_content_evolution(self, strategy_id: int, time_period: str = "30d") -> Dict[str, Any]:
|
||||
async def analyze_content_evolution(self, user_id: int, strategy_id: int, time_period: str = "30d") -> Dict[str, Any]:
|
||||
"""Analyze content evolution over time for a specific strategy."""
|
||||
try:
|
||||
logger.info(f"Starting content evolution analysis for strategy {strategy_id}")
|
||||
logger.info(f"Starting content evolution analysis for strategy {strategy_id} (user {user_id})")
|
||||
|
||||
# Perform content evolution analysis
|
||||
evolution_analysis = await self.ai_analytics_service.analyze_content_evolution(
|
||||
user_id=user_id,
|
||||
strategy_id=strategy_id,
|
||||
time_period=time_period
|
||||
)
|
||||
@@ -55,13 +57,14 @@ class ContentPlanningAIAnalyticsService:
|
||||
logger.error(f"Error analyzing content evolution: {str(e)}")
|
||||
raise ContentPlanningErrorHandler.handle_general_error(e, "analyze_content_evolution")
|
||||
|
||||
async def analyze_performance_trends(self, strategy_id: int, metrics: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||
async def analyze_performance_trends(self, user_id: int, strategy_id: int, metrics: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||
"""Analyze performance trends for content strategy."""
|
||||
try:
|
||||
logger.info(f"Starting performance trends analysis for strategy {strategy_id}")
|
||||
logger.info(f"Starting performance trends analysis for strategy {strategy_id} (user {user_id})")
|
||||
|
||||
# Perform performance trends analysis
|
||||
trends_analysis = await self.ai_analytics_service.analyze_performance_trends(
|
||||
user_id=user_id,
|
||||
strategy_id=strategy_id,
|
||||
metrics=metrics
|
||||
)
|
||||
@@ -191,24 +194,31 @@ class ContentPlanningAIAnalyticsService:
|
||||
# 🚨 CRITICAL: Always run fresh AI analysis for refresh operations
|
||||
logger.info(f"🔄 Running FRESH AI analysis for user {current_user_id} (force_refresh: {force_refresh})")
|
||||
|
||||
# Get personalized inputs from onboarding data
|
||||
personalized_inputs = self.onboarding_service.get_personalized_ai_inputs(current_user_id)
|
||||
# Get personalized inputs from onboarding data (SSOT)
|
||||
db = SessionLocal()
|
||||
try:
|
||||
personalized_inputs = await self.onboarding_integration_service.process_onboarding_data(str(current_user_id), db)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
logger.info(f"📊 Using personalized inputs: {len(personalized_inputs)} data points")
|
||||
|
||||
# Generate real AI insights using personalized data
|
||||
logger.info("🔍 Generating performance analysis...")
|
||||
performance_analysis = await self.ai_analytics_service.analyze_performance_trends(
|
||||
user_id=current_user_id,
|
||||
strategy_id=strategy_id or 1
|
||||
)
|
||||
|
||||
logger.info("🧠 Generating strategic intelligence...")
|
||||
strategic_intelligence = await self.ai_analytics_service.generate_strategic_intelligence(
|
||||
user_id=current_user_id,
|
||||
strategy_id=strategy_id or 1
|
||||
)
|
||||
|
||||
logger.info("📈 Analyzing content evolution...")
|
||||
evolution_analysis = await self.ai_analytics_service.analyze_content_evolution(
|
||||
user_id=current_user_id,
|
||||
strategy_id=strategy_id or 1
|
||||
)
|
||||
|
||||
@@ -255,9 +265,9 @@ class ContentPlanningAIAnalyticsService:
|
||||
"data_source": "ai_analysis",
|
||||
"user_profile": {
|
||||
"website_url": personalized_inputs.get('website_analysis', {}).get('website_url', ''),
|
||||
"content_types": personalized_inputs.get('website_analysis', {}).get('content_types', []),
|
||||
"target_audience": personalized_inputs.get('website_analysis', {}).get('target_audience', []),
|
||||
"industry_focus": personalized_inputs.get('website_analysis', {}).get('industry_focus', 'general')
|
||||
"content_types": personalized_inputs.get('canonical_profile', {}).get('content_types', []),
|
||||
"target_audience": personalized_inputs.get('canonical_profile', {}).get('target_audience', []),
|
||||
"industry_focus": personalized_inputs.get('canonical_profile', {}).get('industry', 'general')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,27 +75,27 @@ class AIStrategyGenerator:
|
||||
base_strategy = await self._generate_base_strategy_fields(user_id, context)
|
||||
|
||||
# Step 2: Generate strategic insights and recommendations
|
||||
strategic_insights = await self._generate_strategic_insights(base_strategy, context)
|
||||
strategic_insights = await self._generate_strategic_insights(base_strategy, context, user_id=user_id)
|
||||
if strategic_insights.get("ai_generation_failed"):
|
||||
failed_components.append("strategic_insights")
|
||||
|
||||
# Step 3: Generate competitive analysis
|
||||
competitive_analysis = await self._generate_competitive_analysis(base_strategy, context)
|
||||
competitive_analysis = await self._generate_competitive_analysis(base_strategy, context, user_id=user_id)
|
||||
if competitive_analysis.get("ai_generation_failed"):
|
||||
failed_components.append("competitive_analysis")
|
||||
|
||||
# Step 4: Generate performance predictions
|
||||
performance_predictions = await self._generate_performance_predictions(base_strategy, context)
|
||||
performance_predictions = await self._generate_performance_predictions(base_strategy, context, user_id=user_id)
|
||||
if performance_predictions.get("ai_generation_failed"):
|
||||
failed_components.append("performance_predictions")
|
||||
|
||||
# Step 5: Generate implementation roadmap
|
||||
implementation_roadmap = await self._generate_implementation_roadmap(base_strategy, context)
|
||||
implementation_roadmap = await self._generate_implementation_roadmap(base_strategy, context, user_id=user_id)
|
||||
if implementation_roadmap.get("ai_generation_failed"):
|
||||
failed_components.append("implementation_roadmap")
|
||||
|
||||
# Step 6: Generate risk assessment
|
||||
risk_assessment = await self._generate_risk_assessment(base_strategy, context)
|
||||
risk_assessment = await self._generate_risk_assessment(base_strategy, context, user_id=user_id)
|
||||
if risk_assessment.get("ai_generation_failed"):
|
||||
failed_components.append("risk_assessment")
|
||||
|
||||
@@ -169,7 +169,7 @@ class AIStrategyGenerator:
|
||||
self.logger.error(f"Error generating base strategy fields: {str(e)}")
|
||||
raise
|
||||
|
||||
async def _generate_strategic_insights(self, base_strategy: Dict[str, Any], context: Dict[str, Any], ai_manager: Optional[Any] = None) -> Dict[str, Any]:
|
||||
async def _generate_strategic_insights(self, base_strategy: Dict[str, Any], context: Dict[str, Any], user_id: Optional[int] = None, ai_manager: Optional[Any] = None) -> Dict[str, Any]:
|
||||
"""Generate strategic insights using AI."""
|
||||
try:
|
||||
logger.info("🧠 Generating strategic insights...")
|
||||
@@ -222,7 +222,8 @@ class AIStrategyGenerator:
|
||||
response = await ai_manager.execute_structured_json_call(
|
||||
AIServiceType.STRATEGIC_INTELLIGENCE,
|
||||
prompt,
|
||||
schema
|
||||
schema,
|
||||
user_id=str(user_id) if user_id else None
|
||||
)
|
||||
|
||||
if not response or not response.get("data"):
|
||||
@@ -306,7 +307,8 @@ class AIStrategyGenerator:
|
||||
response = await ai_manager.execute_structured_json_call(
|
||||
AIServiceType.MARKET_POSITION_ANALYSIS,
|
||||
prompt,
|
||||
schema
|
||||
schema,
|
||||
user_id=str(user_id) if user_id else None
|
||||
)
|
||||
|
||||
if not response or not response.get("data"):
|
||||
@@ -339,7 +341,7 @@ class AIStrategyGenerator:
|
||||
"failure_reason": str(e)
|
||||
}
|
||||
|
||||
async def _generate_content_calendar(self, base_strategy: Dict[str, Any], context: Dict[str, Any], ai_manager: Optional[Any] = None) -> Dict[str, Any]:
|
||||
async def _generate_content_calendar(self, base_strategy: Dict[str, Any], context: Dict[str, Any], user_id: Optional[int] = None, ai_manager: Optional[Any] = None) -> Dict[str, Any]:
|
||||
"""Generate content calendar using AI."""
|
||||
try:
|
||||
logger.info("📅 Generating content calendar...")
|
||||
@@ -442,7 +444,8 @@ class AIStrategyGenerator:
|
||||
response = await ai_manager.execute_structured_json_call(
|
||||
AIServiceType.CONTENT_SCHEDULE_GENERATION,
|
||||
prompt,
|
||||
schema
|
||||
schema,
|
||||
user_id=str(user_id) if user_id else None
|
||||
)
|
||||
|
||||
if not response or not response.get("data"):
|
||||
@@ -455,7 +458,7 @@ class AIStrategyGenerator:
|
||||
logger.error(f"❌ Error generating content calendar: {str(e)}")
|
||||
raise RuntimeError(f"Failed to generate content calendar: {str(e)}")
|
||||
|
||||
async def _generate_performance_predictions(self, base_strategy: Dict[str, Any], context: Dict[str, Any], ai_manager: Optional[Any] = None) -> Dict[str, Any]:
|
||||
async def _generate_performance_predictions(self, base_strategy: Dict[str, Any], context: Dict[str, Any], user_id: Optional[int] = None, ai_manager: Optional[Any] = None) -> Dict[str, Any]:
|
||||
"""Generate performance predictions using AI."""
|
||||
try:
|
||||
logger.info("📊 Generating performance predictions...")
|
||||
@@ -525,7 +528,8 @@ class AIStrategyGenerator:
|
||||
response = await ai_manager.execute_structured_json_call(
|
||||
AIServiceType.PERFORMANCE_PREDICTION,
|
||||
prompt,
|
||||
schema
|
||||
schema,
|
||||
user_id=str(user_id) if user_id else None
|
||||
)
|
||||
|
||||
if not response or not response.get("data"):
|
||||
@@ -551,7 +555,7 @@ class AIStrategyGenerator:
|
||||
"failure_reason": str(e)
|
||||
}
|
||||
|
||||
async def _generate_implementation_roadmap(self, base_strategy: Dict[str, Any], context: Dict[str, Any], ai_manager: Optional[Any] = None) -> Dict[str, Any]:
|
||||
async def _generate_implementation_roadmap(self, base_strategy: Dict[str, Any], context: Dict[str, Any], user_id: Optional[int] = None, ai_manager: Optional[Any] = None) -> Dict[str, Any]:
|
||||
"""Generate implementation roadmap using AI."""
|
||||
try:
|
||||
logger.info("🗺️ Generating implementation roadmap...")
|
||||
|
||||
@@ -10,7 +10,6 @@ from sqlalchemy.orm import Session
|
||||
|
||||
# Import database models
|
||||
from models.enhanced_strategy_models import EnhancedContentStrategy, EnhancedAIAnalysisResult, OnboardingDataIntegration
|
||||
from models.onboarding import OnboardingSession, WebsiteAnalysis, ResearchPreferences, APIKey
|
||||
|
||||
# Import modular services
|
||||
from ..ai_analysis.ai_recommendations import AIRecommendationsService
|
||||
@@ -177,7 +176,7 @@ class EnhancedStrategyService:
|
||||
db.rollback()
|
||||
raise
|
||||
|
||||
async def get_enhanced_strategies(self, user_id: Optional[int] = None, strategy_id: Optional[int] = None, db: Session = None) -> Dict[str, Any]:
|
||||
async def get_enhanced_strategies(self, user_id: Optional[str] = None, strategy_id: Optional[int] = None, db: Session = None) -> Dict[str, Any]:
|
||||
"""Get enhanced content strategies with comprehensive data and AI recommendations."""
|
||||
try:
|
||||
logger.info(f"🚀 Starting enhanced strategy analysis for user: {user_id}, strategy: {strategy_id}")
|
||||
@@ -261,102 +260,115 @@ class EnhancedStrategyService:
|
||||
logger.error(f"❌ Error retrieving enhanced strategies: {str(e)}")
|
||||
raise
|
||||
|
||||
async def _enhance_strategy_with_onboarding_data(self, strategy: EnhancedContentStrategy, user_id: int, db: Session) -> None:
|
||||
"""Enhance strategy with intelligent auto-population from onboarding data."""
|
||||
async def _enhance_strategy_with_onboarding_data(self, strategy: EnhancedContentStrategy, user_id: str, db: Session) -> None:
|
||||
"""Enhance strategy with intelligent auto-population from canonical onboarding data."""
|
||||
try:
|
||||
logger.info(f"Enhancing strategy with onboarding data for user: {user_id}")
|
||||
|
||||
# Get onboarding session
|
||||
onboarding_session = db.query(OnboardingSession).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not onboarding_session:
|
||||
logger.info("No onboarding session found for user")
|
||||
return
|
||||
|
||||
# Get website analysis data
|
||||
website_analysis = db.query(WebsiteAnalysis).filter(
|
||||
WebsiteAnalysis.session_id == onboarding_session.id
|
||||
).first()
|
||||
|
||||
# Get research preferences data
|
||||
research_preferences = db.query(ResearchPreferences).filter(
|
||||
ResearchPreferences.session_id == onboarding_session.id
|
||||
).first()
|
||||
|
||||
# Get API keys data
|
||||
api_keys = db.query(APIKey).filter(
|
||||
APIKey.session_id == onboarding_session.id
|
||||
).all()
|
||||
|
||||
# Auto-populate fields from onboarding data
|
||||
|
||||
integrated_data = await self.onboarding_data_service.process_onboarding_data(user_id, db)
|
||||
canonical_profile = integrated_data.get('canonical_profile') or {}
|
||||
|
||||
website_analysis = integrated_data.get('website_analysis') or {}
|
||||
research_preferences = integrated_data.get('research_preferences') or {}
|
||||
competitor_analysis = integrated_data.get('competitor_analysis') or []
|
||||
api_keys_data = integrated_data.get('api_keys_data') or {}
|
||||
|
||||
auto_populated_fields = {}
|
||||
data_sources = {}
|
||||
|
||||
if website_analysis:
|
||||
# Extract content preferences from writing style
|
||||
if website_analysis.writing_style:
|
||||
strategy.content_preferences = extract_content_preferences_from_style(
|
||||
website_analysis.writing_style
|
||||
)
|
||||
|
||||
# Prioritize Canonical Profile for merged insights
|
||||
if canonical_profile:
|
||||
if canonical_profile.get('target_audience'):
|
||||
strategy.target_audience = canonical_profile.get('target_audience')
|
||||
auto_populated_fields['target_audience'] = 'canonical_profile'
|
||||
|
||||
if canonical_profile.get('industry'):
|
||||
strategy.industry = canonical_profile.get('industry')
|
||||
auto_populated_fields['industry'] = 'canonical_profile'
|
||||
|
||||
if canonical_profile.get('content_types'):
|
||||
strategy.preferred_formats = canonical_profile.get('content_types')
|
||||
auto_populated_fields['preferred_formats'] = 'canonical_profile'
|
||||
|
||||
if isinstance(website_analysis, dict) and website_analysis:
|
||||
writing_style = website_analysis.get('writing_style') or {}
|
||||
if isinstance(writing_style, dict) and writing_style:
|
||||
strategy.content_preferences = extract_content_preferences_from_style(writing_style)
|
||||
auto_populated_fields['content_preferences'] = 'website_analysis'
|
||||
|
||||
# Extract target audience from analysis
|
||||
if website_analysis.target_audience:
|
||||
strategy.target_audience = website_analysis.target_audience
|
||||
auto_populated_fields['target_audience'] = 'website_analysis'
|
||||
|
||||
# Extract brand voice from style guidelines
|
||||
if website_analysis.style_guidelines:
|
||||
strategy.brand_voice = extract_brand_voice_from_guidelines(
|
||||
website_analysis.style_guidelines
|
||||
)
|
||||
|
||||
# Fallback to website_analysis if not in canonical_profile
|
||||
if 'target_audience' not in auto_populated_fields:
|
||||
target_audience = website_analysis.get('target_audience')
|
||||
if target_audience:
|
||||
strategy.target_audience = target_audience
|
||||
auto_populated_fields['target_audience'] = 'website_analysis'
|
||||
|
||||
style_guidelines = website_analysis.get('style_guidelines') or {}
|
||||
if isinstance(style_guidelines, dict) and style_guidelines:
|
||||
strategy.brand_voice = extract_brand_voice_from_guidelines(style_guidelines)
|
||||
auto_populated_fields['brand_voice'] = 'website_analysis'
|
||||
|
||||
data_sources['website_analysis'] = website_analysis.to_dict()
|
||||
|
||||
if research_preferences:
|
||||
# Extract content types from research preferences
|
||||
if research_preferences.content_types:
|
||||
strategy.preferred_formats = research_preferences.content_types
|
||||
auto_populated_fields['preferred_formats'] = 'research_preferences'
|
||||
|
||||
# Extract writing style from preferences
|
||||
if research_preferences.writing_style:
|
||||
strategy.editorial_guidelines = extract_editorial_guidelines_from_style(
|
||||
research_preferences.writing_style
|
||||
)
|
||||
|
||||
data_sources['website_analysis'] = website_analysis
|
||||
|
||||
if isinstance(research_preferences, dict) and research_preferences:
|
||||
# Fallback to research_preferences if not in canonical_profile
|
||||
if 'preferred_formats' not in auto_populated_fields:
|
||||
content_types = research_preferences.get('content_types')
|
||||
if content_types:
|
||||
strategy.preferred_formats = content_types
|
||||
auto_populated_fields['preferred_formats'] = 'research_preferences'
|
||||
|
||||
prefs_writing_style = research_preferences.get('writing_style') or {}
|
||||
if isinstance(prefs_writing_style, dict) and prefs_writing_style:
|
||||
strategy.editorial_guidelines = extract_editorial_guidelines_from_style(prefs_writing_style)
|
||||
auto_populated_fields['editorial_guidelines'] = 'research_preferences'
|
||||
|
||||
data_sources['research_preferences'] = research_preferences
|
||||
|
||||
# Integrate Competitor Analysis (Step 3)
|
||||
if competitor_analysis:
|
||||
competitors = []
|
||||
for comp in competitor_analysis:
|
||||
# Prefer domain, then title, then url
|
||||
# Handle both dict and object (though integrated_data usually returns dicts via to_dict)
|
||||
if isinstance(comp, dict):
|
||||
name = comp.get('competitor_domain') or comp.get('title') or comp.get('competitor_url')
|
||||
else:
|
||||
name = getattr(comp, 'competitor_domain', None) or getattr(comp, 'competitor_url', None)
|
||||
|
||||
if name:
|
||||
competitors.append(name)
|
||||
|
||||
data_sources['research_preferences'] = research_preferences.to_dict()
|
||||
|
||||
# Create onboarding data integration record
|
||||
if competitors:
|
||||
# Limit to top 10 to avoid overwhelming the strategy
|
||||
strategy.top_competitors = competitors[:10]
|
||||
auto_populated_fields['top_competitors'] = 'competitor_analysis'
|
||||
data_sources['competitor_analysis'] = competitor_analysis
|
||||
|
||||
integration = OnboardingDataIntegration(
|
||||
user_id=user_id,
|
||||
strategy_id=strategy.id,
|
||||
website_analysis_data=data_sources.get('website_analysis'),
|
||||
research_preferences_data=data_sources.get('research_preferences'),
|
||||
api_keys_data=[key.to_dict() for key in api_keys] if api_keys else None,
|
||||
api_keys_data=api_keys_data,
|
||||
auto_populated_fields=auto_populated_fields,
|
||||
field_mappings=create_field_mappings(),
|
||||
data_quality_scores=calculate_data_quality_scores(data_sources),
|
||||
confidence_levels={}, # Will be calculated by data quality service
|
||||
data_freshness={} # Will be calculated by data quality service
|
||||
confidence_levels={},
|
||||
data_freshness={}
|
||||
)
|
||||
|
||||
|
||||
db.add(integration)
|
||||
db.commit()
|
||||
|
||||
# Update strategy with onboarding data used
|
||||
|
||||
strategy.onboarding_data_used = {
|
||||
'auto_populated_fields': auto_populated_fields,
|
||||
'data_sources': list(data_sources.keys()),
|
||||
'integration_id': integration.id
|
||||
}
|
||||
|
||||
|
||||
logger.info(f"Strategy enhanced with onboarding data: {len(auto_populated_fields)} fields auto-populated")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error enhancing strategy with onboarding data: {str(e)}")
|
||||
# Don't raise error, just log it as this is enhancement, not core functionality
|
||||
@@ -581,4 +593,4 @@ class EnhancedStrategyService:
|
||||
def _convert_to_xml(self, data: Dict[str, Any]) -> str:
|
||||
"""Convert data to XML format (placeholder implementation)."""
|
||||
# This would be implemented with proper XML conversion
|
||||
return f"<strategy>{str(data)}</strategy>"
|
||||
return f"<strategy>{str(data)}</strategy>"
|
||||
|
||||
@@ -3,7 +3,7 @@ Onboarding Data Integration Service
|
||||
Onboarding data integration and processing.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from utils.logger_utils import get_service_logger
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -19,11 +19,16 @@ from models.onboarding import (
|
||||
ResearchPreferences,
|
||||
APIKey,
|
||||
PersonaData,
|
||||
CompetitorAnalysis
|
||||
CompetitorAnalysis,
|
||||
SEOPageAudit
|
||||
)
|
||||
from models.website_analysis_monitoring_models import (
|
||||
DeepCompetitorAnalysisTask,
|
||||
DeepCompetitorAnalysisExecutionLog
|
||||
)
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_service_logger("onboarding.data_integration")
|
||||
|
||||
class OnboardingDataIntegrationService:
|
||||
"""Service for onboarding data integration and processing."""
|
||||
@@ -32,6 +37,162 @@ class OnboardingDataIntegrationService:
|
||||
self.data_freshness_threshold = timedelta(hours=24)
|
||||
self.max_analysis_age = timedelta(days=7)
|
||||
|
||||
def get_integrated_data_sync(self, user_id: str, db: Session) -> Dict[str, Any]:
|
||||
"""Synchronous version of process_onboarding_data for sync contexts.
|
||||
Note: Does not include async data sources like GSC/Bing analytics.
|
||||
"""
|
||||
try:
|
||||
# Get all onboarding data sources (DB only)
|
||||
website_analysis = self._get_website_analysis(user_id, db)
|
||||
research_preferences = self._get_research_preferences(user_id, db)
|
||||
api_keys_data = self._get_api_keys_data(user_id, db)
|
||||
onboarding_session = self._get_onboarding_session(user_id, db)
|
||||
persona_data = self._get_persona_data(user_id, db)
|
||||
competitor_analysis = self._get_competitor_analysis(user_id, db)
|
||||
deep_competitor_analysis = self._get_deep_competitor_analysis(user_id, db)
|
||||
|
||||
# Skip async sources
|
||||
gsc_analytics = {}
|
||||
bing_analytics = {}
|
||||
|
||||
canonical_profile = self._build_canonical_profile(
|
||||
website_analysis,
|
||||
research_preferences,
|
||||
persona_data,
|
||||
onboarding_session,
|
||||
competitor_analysis,
|
||||
deep_competitor_analysis
|
||||
)
|
||||
|
||||
integrated_data = {
|
||||
'website_analysis': website_analysis,
|
||||
'research_preferences': research_preferences,
|
||||
'api_keys_data': api_keys_data,
|
||||
'onboarding_session': onboarding_session,
|
||||
'persona_data': persona_data,
|
||||
'competitor_analysis': competitor_analysis,
|
||||
'deep_competitor_analysis': deep_competitor_analysis,
|
||||
'gsc_analytics': gsc_analytics,
|
||||
'bing_analytics': bing_analytics,
|
||||
'canonical_profile': canonical_profile,
|
||||
'data_quality': self._assess_data_quality(website_analysis, research_preferences, api_keys_data, persona_data, competitor_analysis, gsc_analytics, bing_analytics),
|
||||
'processing_timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
return integrated_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing onboarding data (sync) for user {user_id}: {str(e)}")
|
||||
return self._get_fallback_data()
|
||||
|
||||
async def refresh_integrated_data(self, user_id: str, db: Session) -> None:
|
||||
"""
|
||||
Refresh and store integrated data (DB-only sources) to ensure SSOT is up-to-date.
|
||||
This is a lightweight version of process_onboarding_data suitable for calling
|
||||
after individual step completion.
|
||||
"""
|
||||
try:
|
||||
# Re-use sync logic but await the storage
|
||||
integrated_data = self.get_integrated_data_sync(user_id, db)
|
||||
await self._store_integrated_data(user_id, integrated_data, db)
|
||||
logger.info(f"Refreshed integrated data (SSOT) for user {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to refresh integrated data for user {user_id}: {e}")
|
||||
# Non-blocking failure
|
||||
|
||||
async def store_competitive_sitemap_benchmarking(self, user_id: str, report: Dict[str, Any], db: Session) -> bool:
|
||||
try:
|
||||
if not user_id:
|
||||
return False
|
||||
if not isinstance(report, dict):
|
||||
return False
|
||||
|
||||
session = db.query(OnboardingSession).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
).order_by(OnboardingSession.updated_at.desc()).first()
|
||||
|
||||
if not session:
|
||||
return False
|
||||
|
||||
website_analysis = db.query(WebsiteAnalysis).filter(
|
||||
WebsiteAnalysis.session_id == session.id
|
||||
).order_by(WebsiteAnalysis.updated_at.desc()).first()
|
||||
|
||||
if not website_analysis:
|
||||
return False
|
||||
|
||||
existing = website_analysis.seo_audit if isinstance(website_analysis.seo_audit, dict) else {}
|
||||
existing["competitive_sitemap_benchmarking"] = report
|
||||
website_analysis.seo_audit = existing
|
||||
website_analysis.updated_at = datetime.utcnow()
|
||||
|
||||
# Use flag_modified to ensure JSON update is detected by SQLAlchemy
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
flag_modified(website_analysis, "seo_audit")
|
||||
|
||||
db.commit()
|
||||
|
||||
try:
|
||||
await self.refresh_integrated_data(user_id, db)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to store competitive sitemap benchmarking for user {user_id}: {e}")
|
||||
db.rollback()
|
||||
return False
|
||||
|
||||
async def update_competitive_sitemap_benchmarking_status(self, user_id: str, status: str, db: Session, error: Optional[str] = None) -> bool:
|
||||
"""Update the status of the competitive sitemap benchmarking task."""
|
||||
try:
|
||||
if not user_id:
|
||||
return False
|
||||
|
||||
session = db.query(OnboardingSession).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
).order_by(OnboardingSession.updated_at.desc()).first()
|
||||
|
||||
if not session:
|
||||
return False
|
||||
|
||||
website_analysis = db.query(WebsiteAnalysis).filter(
|
||||
WebsiteAnalysis.session_id == session.id
|
||||
).order_by(WebsiteAnalysis.updated_at.desc()).first()
|
||||
|
||||
if not website_analysis:
|
||||
return False
|
||||
|
||||
existing = website_analysis.seo_audit if isinstance(website_analysis.seo_audit, dict) else {}
|
||||
|
||||
# Get existing benchmarking data or initialize
|
||||
benchmarking = existing.get("competitive_sitemap_benchmarking", {})
|
||||
if not isinstance(benchmarking, dict):
|
||||
benchmarking = {}
|
||||
|
||||
benchmarking["status"] = status
|
||||
if error:
|
||||
benchmarking["error"] = error
|
||||
if status == "processing":
|
||||
benchmarking["started_at"] = datetime.utcnow().isoformat()
|
||||
|
||||
existing["competitive_sitemap_benchmarking"] = benchmarking
|
||||
website_analysis.seo_audit = existing
|
||||
# Force update flag if needed, but assignment should trigger it
|
||||
website_analysis.updated_at = datetime.utcnow()
|
||||
|
||||
# Use flag_modified if using JSON type with SQLAlchemy to ensure update
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
flag_modified(website_analysis, "seo_audit")
|
||||
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update competitive sitemap benchmarking status for user {user_id}: {e}")
|
||||
if db:
|
||||
db.rollback()
|
||||
return False
|
||||
|
||||
async def process_onboarding_data(self, user_id: str, db: Session) -> Dict[str, Any]:
|
||||
"""Process and integrate all onboarding data for a user.
|
||||
|
||||
@@ -49,6 +210,7 @@ class OnboardingDataIntegrationService:
|
||||
onboarding_session = self._get_onboarding_session(user_id, db)
|
||||
persona_data = self._get_persona_data(user_id, db)
|
||||
competitor_analysis = self._get_competitor_analysis(user_id, db)
|
||||
deep_competitor_analysis = self._get_deep_competitor_analysis(user_id, db)
|
||||
gsc_analytics = await self._get_gsc_analytics(user_id)
|
||||
bing_analytics = await self._get_bing_analytics(user_id)
|
||||
|
||||
@@ -63,7 +225,15 @@ class OnboardingDataIntegrationService:
|
||||
logger.info(f" - GSC Analytics: {'✅ Found' if gsc_analytics else '❌ Missing'}")
|
||||
logger.info(f" - Bing Analytics: {'✅ Found' if bing_analytics else '❌ Missing'}")
|
||||
|
||||
# Process and integrate data
|
||||
canonical_profile = self._build_canonical_profile(
|
||||
website_analysis,
|
||||
research_preferences,
|
||||
persona_data,
|
||||
onboarding_session,
|
||||
competitor_analysis,
|
||||
deep_competitor_analysis
|
||||
)
|
||||
|
||||
integrated_data = {
|
||||
'website_analysis': website_analysis,
|
||||
'research_preferences': research_preferences,
|
||||
@@ -71,8 +241,10 @@ class OnboardingDataIntegrationService:
|
||||
'onboarding_session': onboarding_session,
|
||||
'persona_data': persona_data,
|
||||
'competitor_analysis': competitor_analysis,
|
||||
'deep_competitor_analysis': deep_competitor_analysis,
|
||||
'gsc_analytics': gsc_analytics,
|
||||
'bing_analytics': bing_analytics,
|
||||
'canonical_profile': canonical_profile,
|
||||
'data_quality': self._assess_data_quality(website_analysis, research_preferences, api_keys_data, persona_data, competitor_analysis, gsc_analytics, bing_analytics),
|
||||
'processing_timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
@@ -105,7 +277,7 @@ class OnboardingDataIntegrationService:
|
||||
).order_by(OnboardingSession.updated_at.desc()).first()
|
||||
|
||||
if not session:
|
||||
logger.warning(f"No onboarding session found for user {user_id}")
|
||||
logger.info(f"No onboarding session found for user {user_id}")
|
||||
return {}
|
||||
|
||||
# Get the latest website analysis for this session
|
||||
@@ -114,13 +286,17 @@ class OnboardingDataIntegrationService:
|
||||
).order_by(WebsiteAnalysis.updated_at.desc()).first()
|
||||
|
||||
if not website_analysis:
|
||||
logger.warning(f"No website analysis found for user {user_id}")
|
||||
logger.info(f"No website analysis found for user {user_id}")
|
||||
return {}
|
||||
|
||||
# Convert to dictionary and add metadata
|
||||
analysis_data = website_analysis.to_dict()
|
||||
analysis_data['data_freshness'] = self._calculate_freshness(website_analysis.updated_at)
|
||||
analysis_data['confidence_level'] = 0.9 if website_analysis.status == 'completed' else 0.5
|
||||
|
||||
site_url = website_analysis.website_url
|
||||
if site_url:
|
||||
analysis_data["full_site_seo_summary"] = self._get_full_site_seo_summary(user_id, site_url, db)
|
||||
|
||||
logger.info(f"Retrieved website analysis for user {user_id}: {website_analysis.website_url}")
|
||||
return analysis_data
|
||||
@@ -129,6 +305,36 @@ class OnboardingDataIntegrationService:
|
||||
logger.error(f"Error getting website analysis for user {user_id}: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _get_full_site_seo_summary(self, user_id: str, website_url: str, db: Session) -> Dict[str, Any]:
|
||||
try:
|
||||
rows = db.query(SEOPageAudit).filter(
|
||||
SEOPageAudit.user_id == user_id,
|
||||
SEOPageAudit.website_url == website_url
|
||||
).all()
|
||||
|
||||
if not rows:
|
||||
return {}
|
||||
|
||||
scored = [r for r in rows if r.overall_score is not None]
|
||||
scores = [int(r.overall_score) for r in scored if isinstance(r.overall_score, (int, float))]
|
||||
avg_score = round(sum(scores) / len(scores), 1) if scores else 0
|
||||
|
||||
fix_scheduled_count = len([r for r in scored if (r.status or "").lower() == "fix_scheduled"])
|
||||
|
||||
worst = sorted(scored, key=lambda r: r.overall_score if r.overall_score is not None else 10**9)[:5]
|
||||
worst_pages = [{"page_url": r.page_url, "overall_score": r.overall_score, "status": r.status} for r in worst]
|
||||
|
||||
return {
|
||||
"pages_audited": len(rows),
|
||||
"pages_scored": len(scored),
|
||||
"avg_score": avg_score,
|
||||
"fix_scheduled_pages": fix_scheduled_count,
|
||||
"worst_pages": worst_pages
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error building full-site SEO summary for user {user_id}: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _get_research_preferences(self, user_id: str, db: Session) -> Dict[str, Any]:
|
||||
"""Get research preferences data for the user."""
|
||||
try:
|
||||
@@ -138,7 +344,7 @@ class OnboardingDataIntegrationService:
|
||||
).order_by(OnboardingSession.updated_at.desc()).first()
|
||||
|
||||
if not session:
|
||||
logger.warning(f"No onboarding session found for user {user_id}")
|
||||
logger.info(f"No onboarding session found for user {user_id}")
|
||||
return {}
|
||||
|
||||
# Get research preferences for this session
|
||||
@@ -147,7 +353,7 @@ class OnboardingDataIntegrationService:
|
||||
).first()
|
||||
|
||||
if not research_prefs:
|
||||
logger.warning(f"No research preferences found for user {user_id}")
|
||||
logger.info(f"No research preferences found for user {user_id}")
|
||||
return {}
|
||||
|
||||
# Convert to dictionary and add metadata
|
||||
@@ -171,7 +377,7 @@ class OnboardingDataIntegrationService:
|
||||
).order_by(OnboardingSession.updated_at.desc()).first()
|
||||
|
||||
if not session:
|
||||
logger.warning(f"No onboarding session found for user {user_id}")
|
||||
logger.info(f"No onboarding session found for user {user_id}")
|
||||
return {}
|
||||
|
||||
# Get all API keys for this session
|
||||
@@ -180,7 +386,7 @@ class OnboardingDataIntegrationService:
|
||||
).all()
|
||||
|
||||
if not api_keys:
|
||||
logger.warning(f"No API keys found for user {user_id}")
|
||||
logger.info(f"No API keys found for user {user_id}")
|
||||
return {}
|
||||
|
||||
# Convert to dictionary format
|
||||
@@ -202,16 +408,14 @@ class OnboardingDataIntegrationService:
|
||||
def _get_onboarding_session(self, user_id: str, db: Session) -> Dict[str, Any]:
|
||||
"""Get onboarding session data for the user."""
|
||||
try:
|
||||
# Get the latest onboarding session for the user
|
||||
session = db.query(OnboardingSession).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
).order_by(OnboardingSession.updated_at.desc()).first()
|
||||
|
||||
if not session:
|
||||
logger.warning(f"No onboarding session found for user {user_id}")
|
||||
logger.info(f"No onboarding session found for user {user_id}")
|
||||
return {}
|
||||
|
||||
# Convert to dictionary
|
||||
session_data = {
|
||||
'id': session.id,
|
||||
'user_id': session.user_id,
|
||||
@@ -225,11 +429,303 @@ class OnboardingDataIntegrationService:
|
||||
|
||||
logger.info(f"Retrieved onboarding session for user {user_id}: step {session.current_step}, progress {session.progress}%")
|
||||
return session_data
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting onboarding session for user {user_id}: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _build_canonical_profile(
|
||||
self,
|
||||
website_analysis: Dict[str, Any],
|
||||
research_preferences: Dict[str, Any],
|
||||
persona_data: Dict[str, Any],
|
||||
onboarding_session: Dict[str, Any],
|
||||
competitor_analysis: List[Dict[str, Any]],
|
||||
deep_competitor_analysis: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
core_persona = None
|
||||
if persona_data:
|
||||
if isinstance(persona_data, dict):
|
||||
core_persona = persona_data.get('corePersona') or persona_data.get('core_persona')
|
||||
|
||||
website_target = {}
|
||||
if website_analysis and isinstance(website_analysis, dict):
|
||||
value = website_analysis.get('target_audience') or {}
|
||||
if isinstance(value, dict):
|
||||
website_target = value
|
||||
|
||||
research_target = {}
|
||||
if research_preferences and isinstance(research_preferences, dict):
|
||||
value = research_preferences.get('target_audience') or {}
|
||||
if isinstance(value, dict):
|
||||
research_target = value
|
||||
|
||||
industry = None
|
||||
if core_persona and isinstance(core_persona, dict):
|
||||
value = core_persona.get('industry')
|
||||
if value:
|
||||
industry = value
|
||||
if not industry and website_target:
|
||||
value = website_target.get('industry_focus')
|
||||
if value:
|
||||
industry = value
|
||||
if not industry and research_target:
|
||||
value = research_target.get('industry_focus')
|
||||
if value:
|
||||
industry = value
|
||||
|
||||
target_audience = None
|
||||
target_source = None
|
||||
if core_persona and isinstance(core_persona, dict):
|
||||
value = core_persona.get('target_audience')
|
||||
if value:
|
||||
target_audience = value
|
||||
target_source = 'persona_core'
|
||||
if not target_audience and website_target:
|
||||
value = website_target.get('demographics') or website_target.get('target_audience')
|
||||
if value:
|
||||
target_audience = value
|
||||
target_source = 'website_analysis'
|
||||
if not target_audience and research_target:
|
||||
value = research_target.get('demographics') or research_target.get('target_audience')
|
||||
if value:
|
||||
target_audience = value
|
||||
target_source = 'research_preferences'
|
||||
|
||||
writing_style = {}
|
||||
if website_analysis and isinstance(website_analysis, dict):
|
||||
value = website_analysis.get('writing_style')
|
||||
if isinstance(value, dict):
|
||||
writing_style = value
|
||||
if not writing_style and research_preferences and isinstance(research_preferences, dict):
|
||||
value = research_preferences.get('writing_style')
|
||||
if isinstance(value, dict):
|
||||
writing_style = value
|
||||
|
||||
writing_tone = None
|
||||
writing_voice = None
|
||||
writing_complexity = None
|
||||
writing_engagement = None
|
||||
writing_source = None
|
||||
if writing_style:
|
||||
value = writing_style.get('tone')
|
||||
if value:
|
||||
writing_tone = value
|
||||
|
||||
value = writing_style.get('voice')
|
||||
if value:
|
||||
writing_voice = value
|
||||
|
||||
value = writing_style.get('complexity')
|
||||
if value:
|
||||
writing_complexity = value
|
||||
|
||||
value = writing_style.get('engagement_level')
|
||||
if value:
|
||||
writing_engagement = value
|
||||
|
||||
if website_analysis and website_analysis.get('writing_style'):
|
||||
writing_source = 'website_analysis'
|
||||
elif research_preferences and research_preferences.get('writing_style'):
|
||||
writing_source = 'research_preferences'
|
||||
|
||||
# Brand & Visual Identity
|
||||
brand_colors = []
|
||||
brand_values = []
|
||||
visual_style = {}
|
||||
brand_source = None
|
||||
|
||||
if website_analysis and isinstance(website_analysis, dict):
|
||||
brand_analysis = website_analysis.get('brand_analysis', {})
|
||||
if brand_analysis:
|
||||
brand_colors = brand_analysis.get('color_palette', [])
|
||||
brand_values = brand_analysis.get('brand_values', [])
|
||||
brand_source = 'website_analysis'
|
||||
|
||||
style_guidelines = website_analysis.get('style_guidelines', {})
|
||||
if style_guidelines:
|
||||
visual_style = {
|
||||
'aesthetic': style_guidelines.get('aesthetic'),
|
||||
'visual_style': style_guidelines.get('visual_style')
|
||||
}
|
||||
|
||||
# Content Strategy Insights
|
||||
strategy_insights = {}
|
||||
if website_analysis and isinstance(website_analysis, dict):
|
||||
strategy_insights = website_analysis.get('content_strategy_insights', {})
|
||||
|
||||
seo_profile: Dict[str, Any] = {}
|
||||
if website_analysis and isinstance(website_analysis, dict):
|
||||
seo_profile["homepage_seo_audit"] = website_analysis.get("seo_audit") or {}
|
||||
seo_profile["full_site_seo_summary"] = website_analysis.get("full_site_seo_summary") or {}
|
||||
sitemap_strategy = website_analysis.get("sitemap_strategy_insights")
|
||||
if sitemap_strategy:
|
||||
seo_profile["sitemap_strategy_insights"] = sitemap_strategy
|
||||
|
||||
competitor_seo_benchmarks = self._build_competitor_seo_benchmarks(competitor_analysis)
|
||||
if competitor_seo_benchmarks:
|
||||
seo_profile["competitor_seo_benchmarks"] = competitor_seo_benchmarks
|
||||
|
||||
# Platform Preferences
|
||||
platform_preferences = []
|
||||
platform_source = None
|
||||
|
||||
if core_persona and isinstance(core_persona, dict):
|
||||
# Check persona_data for platforms
|
||||
if isinstance(persona_data, dict):
|
||||
selected = persona_data.get('selectedPlatforms')
|
||||
if selected:
|
||||
platform_preferences = selected
|
||||
platform_source = 'persona_data'
|
||||
else:
|
||||
platform_personas = persona_data.get('platformPersonas')
|
||||
if platform_personas:
|
||||
platform_preferences = list(platform_personas.keys())
|
||||
platform_source = 'persona_data'
|
||||
|
||||
content_types = []
|
||||
content_source = None
|
||||
if research_preferences and isinstance(research_preferences, dict):
|
||||
prefs_content = research_preferences.get('content_types')
|
||||
if isinstance(prefs_content, list):
|
||||
content_types = list(prefs_content)
|
||||
if content_types:
|
||||
content_source = 'research_preferences'
|
||||
if not content_types and website_analysis and isinstance(website_analysis, dict):
|
||||
content_type_data = website_analysis.get('content_type') or {}
|
||||
if isinstance(content_type_data, dict):
|
||||
primary = content_type_data.get('primary_type')
|
||||
if primary:
|
||||
content_types.append(primary)
|
||||
secondary = content_type_data.get('secondary_types')
|
||||
if isinstance(secondary, list):
|
||||
content_types.extend(secondary)
|
||||
if content_types:
|
||||
content_source = 'website_analysis'
|
||||
|
||||
research_depth = None
|
||||
auto_research = None
|
||||
factual_content = None
|
||||
if research_preferences and isinstance(research_preferences, dict):
|
||||
research_depth = research_preferences.get('research_depth')
|
||||
auto_research = research_preferences.get('auto_research')
|
||||
factual_content = research_preferences.get('factual_content')
|
||||
|
||||
business_info = {}
|
||||
if industry:
|
||||
business_info['industry'] = industry
|
||||
if target_audience:
|
||||
business_info['target_audience'] = target_audience
|
||||
|
||||
sources = {
|
||||
'industry': None,
|
||||
'target_audience': target_source,
|
||||
'writing_tone': writing_source,
|
||||
'content_types': content_source,
|
||||
'brand_identity': brand_source,
|
||||
'platform_preferences': platform_source,
|
||||
'seo_profile': 'website_analysis' if website_analysis else None
|
||||
}
|
||||
if core_persona and isinstance(core_persona, dict) and core_persona.get('industry'):
|
||||
sources['industry'] = 'persona_core'
|
||||
elif website_target.get('industry_focus'):
|
||||
sources['industry'] = 'website_analysis'
|
||||
elif research_target.get('industry_focus'):
|
||||
sources['industry'] = 'research_preferences'
|
||||
|
||||
competitive_sitemap_benchmarking = {}
|
||||
try:
|
||||
if website_analysis and isinstance(website_analysis, dict):
|
||||
seo_audit = website_analysis.get("seo_audit")
|
||||
if isinstance(seo_audit, dict):
|
||||
report = seo_audit.get("competitive_sitemap_benchmarking")
|
||||
if isinstance(report, dict):
|
||||
benchmark = report.get("benchmark") if isinstance(report.get("benchmark"), dict) else {}
|
||||
gaps = benchmark.get("gaps") if isinstance(benchmark.get("gaps"), dict) else {}
|
||||
missing_sections = gaps.get("missing_sections") if isinstance(gaps.get("missing_sections"), list) else []
|
||||
competitive_sitemap_benchmarking = {
|
||||
"status": "available",
|
||||
"last_run": report.get("timestamp") or report.get("analysis_date"),
|
||||
"competitors_analyzed": benchmark.get("competitors_analyzed"),
|
||||
"missing_sections_count": len(missing_sections)
|
||||
}
|
||||
except Exception:
|
||||
competitive_sitemap_benchmarking = {}
|
||||
|
||||
competitive_intelligence = {
|
||||
'deep_competitor_analysis': deep_competitor_analysis or {},
|
||||
'competitive_sitemap_benchmarking': competitive_sitemap_benchmarking,
|
||||
'strategic_insights_history': website_analysis.get("strategic_insights_history", []) if isinstance(website_analysis, dict) else []
|
||||
}
|
||||
|
||||
return {
|
||||
'industry': industry,
|
||||
'target_audience': target_audience,
|
||||
'writing_tone': writing_tone or 'professional',
|
||||
'writing_voice': writing_voice or 'authoritative',
|
||||
'writing_complexity': writing_complexity or 'intermediate',
|
||||
'writing_engagement': writing_engagement or 'moderate',
|
||||
'content_types': content_types,
|
||||
'brand_colors': brand_colors,
|
||||
'brand_values': brand_values,
|
||||
'visual_style': visual_style,
|
||||
'strategy_insights': strategy_insights,
|
||||
'seo_profile': seo_profile,
|
||||
'competitive_intelligence': competitive_intelligence,
|
||||
'platform_preferences': platform_preferences,
|
||||
'research_depth': research_depth,
|
||||
'auto_research': auto_research,
|
||||
'factual_content': factual_content,
|
||||
'business_info': business_info,
|
||||
'sources': sources
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error building canonical profile: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _build_competitor_seo_benchmarks(self, competitor_analysis: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
try:
|
||||
if not competitor_analysis:
|
||||
return {}
|
||||
|
||||
rows = []
|
||||
for comp in competitor_analysis:
|
||||
analysis_data = comp.get("analysis_data") if isinstance(comp, dict) else None
|
||||
if not isinstance(analysis_data, dict):
|
||||
continue
|
||||
seo_audit = analysis_data.get("seo_audit")
|
||||
if not isinstance(seo_audit, dict):
|
||||
continue
|
||||
score = seo_audit.get("overall_score")
|
||||
if score is None:
|
||||
continue
|
||||
rows.append({
|
||||
"competitor_url": comp.get("competitor_url") or comp.get("url") or comp.get("website_url"),
|
||||
"competitor_domain": comp.get("competitor_domain") or comp.get("domain"),
|
||||
"overall_score": score,
|
||||
"last_analyzed_at": comp.get("updated_at") or comp.get("analysis_date")
|
||||
})
|
||||
|
||||
if not rows:
|
||||
return {}
|
||||
|
||||
scores = [r["overall_score"] for r in rows if isinstance(r.get("overall_score"), (int, float))]
|
||||
avg_score = round(sum(scores) / len(scores), 1) if scores else None
|
||||
|
||||
best = max(rows, key=lambda r: r.get("overall_score") or 0)
|
||||
worst = min(rows, key=lambda r: r.get("overall_score") or 0)
|
||||
|
||||
return {
|
||||
"competitors_with_seo_audit": len(rows),
|
||||
"avg_homepage_seo_score": avg_score,
|
||||
"best_competitor": best,
|
||||
"worst_competitor": worst
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error building competitor SEO benchmarks: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _assess_data_quality(self, website_analysis: Dict, research_preferences: Dict, api_keys_data: Dict, persona_data: Dict = None, competitor_analysis: List = None, gsc_analytics: Dict = None, bing_analytics: Dict = None) -> Dict[str, Any]:
|
||||
"""Assess the quality and completeness of onboarding data."""
|
||||
try:
|
||||
@@ -432,7 +928,7 @@ class OnboardingDataIntegrationService:
|
||||
).first()
|
||||
|
||||
if not persona:
|
||||
logger.warning(f"No persona data found for user {user_id}")
|
||||
logger.info(f"[Persona] No persona data found for user {user_id}")
|
||||
return {}
|
||||
|
||||
# Convert to dictionary and add metadata
|
||||
@@ -456,10 +952,10 @@ class OnboardingDataIntegrationService:
|
||||
).order_by(OnboardingSession.updated_at.desc()).first()
|
||||
|
||||
if not session:
|
||||
logger.warning(f"🔍 COMPETITOR VALIDATION: No onboarding session found for user {user_id}")
|
||||
logger.info(f"[CompetitorAnalysis] No onboarding session found for user {user_id}")
|
||||
return []
|
||||
|
||||
logger.warning(f"🔍 COMPETITOR VALIDATION: Found session {session.id} for user {user_id}")
|
||||
logger.info(f"[CompetitorAnalysis] user={user_id} session={session.id} (latest)")
|
||||
|
||||
# Get all competitor analyses for this session
|
||||
competitor_records = db.query(CompetitorAnalysis).filter(
|
||||
@@ -467,22 +963,10 @@ class OnboardingDataIntegrationService:
|
||||
).order_by(CompetitorAnalysis.updated_at.desc()).all()
|
||||
|
||||
if not competitor_records:
|
||||
logger.warning(f"🔍 COMPETITOR VALIDATION: No competitor analysis records found for user {user_id}, session {session.id}")
|
||||
logger.warning(f" Checking all sessions for user {user_id}...")
|
||||
# Check all sessions for this user
|
||||
all_sessions = db.query(OnboardingSession).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
).all()
|
||||
logger.warning(f" Total sessions for user: {len(all_sessions)}")
|
||||
for sess in all_sessions:
|
||||
comp_count = db.query(CompetitorAnalysis).filter(
|
||||
CompetitorAnalysis.session_id == sess.id
|
||||
).count()
|
||||
session_timestamp = getattr(sess, 'started_at', None) or getattr(sess, 'updated_at', None)
|
||||
logger.warning(f" Session {sess.id} (timestamp: {session_timestamp}): {comp_count} competitors")
|
||||
logger.info(f"[CompetitorAnalysis] No competitor records found for user={user_id} session={session.id}")
|
||||
return []
|
||||
|
||||
logger.warning(f"🔍 COMPETITOR VALIDATION: Found {len(competitor_records)} competitor records for user {user_id}")
|
||||
logger.info(f"[CompetitorAnalysis] session={session.id} records={len(competitor_records)} user={user_id}")
|
||||
|
||||
# Convert to list of dictionaries
|
||||
# Use to_dict() which includes competitor_url, competitor_domain, analysis_data
|
||||
@@ -496,25 +980,68 @@ class OnboardingDataIntegrationService:
|
||||
competitor_dict['confidence_level'] = 0.9 if record.status == 'completed' else 0.5
|
||||
competitors.append(competitor_dict)
|
||||
|
||||
logger.info(f"Retrieved {len(competitors)} competitor analyses for user {user_id}")
|
||||
logger.info(f"[CompetitorAnalysis] retrieved={len(competitors)} user={user_id}")
|
||||
if competitors:
|
||||
logger.warning(f"🔍 Sample competitor keys: {list(competitors[0].keys())}")
|
||||
logger.warning(f"🔍 Sample competitor has analysis_data: {'analysis_data' in competitors[0]}")
|
||||
if 'analysis_data' in competitors[0]:
|
||||
logger.warning(f"🔍 Sample analysis_data keys: {list(competitors[0]['analysis_data'].keys()) if isinstance(competitors[0]['analysis_data'], dict) else 'Not a dict'}")
|
||||
try:
|
||||
sample = competitors[0]
|
||||
logger.debug(f"[CompetitorAnalysis] sample_keys={list(sample.keys())} has_analysis_data={'analysis_data' in sample}")
|
||||
if isinstance(sample.get('analysis_data'), dict):
|
||||
logger.debug(f"[CompetitorAnalysis] analysis_data_keys={list(sample['analysis_data'].keys())}")
|
||||
except Exception:
|
||||
pass
|
||||
return competitors
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting competitor analysis for user {user_id}: {str(e)}")
|
||||
return []
|
||||
|
||||
def _get_deep_competitor_analysis(self, user_id: str, db: Session) -> Dict[str, Any]:
|
||||
try:
|
||||
task = db.query(DeepCompetitorAnalysisTask).filter(
|
||||
DeepCompetitorAnalysisTask.user_id == user_id
|
||||
).order_by(DeepCompetitorAnalysisTask.updated_at.desc()).first()
|
||||
|
||||
if not task:
|
||||
return {
|
||||
"status": "not_scheduled",
|
||||
"last_run": None,
|
||||
"report": None
|
||||
}
|
||||
|
||||
latest_log = db.query(DeepCompetitorAnalysisExecutionLog).filter(
|
||||
DeepCompetitorAnalysisExecutionLog.task_id == task.id
|
||||
).order_by(DeepCompetitorAnalysisExecutionLog.execution_date.desc()).first()
|
||||
|
||||
last_run = None
|
||||
if latest_log and latest_log.execution_date:
|
||||
last_run = latest_log.execution_date.isoformat()
|
||||
|
||||
report = None
|
||||
if latest_log and latest_log.status == "success":
|
||||
report = latest_log.result_data
|
||||
|
||||
payload = task.payload if isinstance(task.payload, dict) else {}
|
||||
competitors = payload.get("competitors") if isinstance(payload, dict) else None
|
||||
|
||||
return {
|
||||
"status": task.status,
|
||||
"next_execution": task.next_execution.isoformat() if task.next_execution else None,
|
||||
"last_run": last_run,
|
||||
"last_status": latest_log.status if latest_log else None,
|
||||
"competitors_count": len(competitors) if isinstance(competitors, list) else None,
|
||||
"report": report
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting deep competitor analysis for user {user_id}: {str(e)}")
|
||||
return {}
|
||||
|
||||
async def _get_gsc_analytics(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Get Google Search Console analytics data for the user."""
|
||||
try:
|
||||
from services.seo.dashboard_service import SEODashboardService
|
||||
from services.database import get_db_session
|
||||
|
||||
db = get_db_session()
|
||||
db = get_db_session(user_id)
|
||||
try:
|
||||
dashboard_service = SEODashboardService(db)
|
||||
gsc_data = await dashboard_service.get_gsc_data(user_id)
|
||||
@@ -545,7 +1072,7 @@ class OnboardingDataIntegrationService:
|
||||
from services.bing_analytics_storage_service import BingAnalyticsStorageService
|
||||
from services.database import get_db_session
|
||||
|
||||
db = get_db_session()
|
||||
db = get_db_session(user_id)
|
||||
try:
|
||||
dashboard_service = SEODashboardService(db)
|
||||
bing_data = await dashboard_service.get_bing_data(user_id)
|
||||
@@ -553,13 +1080,15 @@ class OnboardingDataIntegrationService:
|
||||
db.close()
|
||||
|
||||
# Also try to get from storage service for more detailed metrics
|
||||
bing_storage = BingAnalyticsStorageService(os.getenv('DATABASE_URL', 'sqlite:///alwrity.db'))
|
||||
from services.database import get_user_db_path
|
||||
db_path = get_user_db_path(user_id)
|
||||
bing_storage = BingAnalyticsStorageService(f'sqlite:///{db_path}')
|
||||
|
||||
# Get site URL from onboarding session if available
|
||||
site_url = None
|
||||
try:
|
||||
from services.database import get_db_session
|
||||
with get_db_session() as db:
|
||||
with get_db_session(user_id) as db:
|
||||
session = db.query(OnboardingSession).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
).order_by(OnboardingSession.updated_at.desc()).first()
|
||||
@@ -663,4 +1192,4 @@ class OnboardingDataIntegrationService:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting integrated data for user {user_id}: {str(e)}")
|
||||
return None
|
||||
return None
|
||||
|
||||
@@ -195,14 +195,29 @@ class DataProcessorService:
|
||||
}
|
||||
|
||||
# Competitive Intelligence Fields
|
||||
# Extract competitors from competitor_analysis list in processed_data
|
||||
competitors_list = processed_data.get('competitor_analysis', [])
|
||||
competitor_names = []
|
||||
|
||||
if competitors_list:
|
||||
for comp in competitors_list:
|
||||
# Try to get domain or title, fallback to URL
|
||||
name = comp.get('competitor_domain') or comp.get('domain') or comp.get('title') or comp.get('competitor_url') or comp.get('url')
|
||||
if name:
|
||||
competitor_names.append(name)
|
||||
|
||||
# Fallback to website_analysis competitors if available (legacy/manual entry)
|
||||
if not competitor_names and website_data.get('competitors'):
|
||||
competitor_names = website_data.get('competitors')
|
||||
|
||||
fields['top_competitors'] = {
|
||||
'value': website_data.get('competitors', [
|
||||
'value': competitor_names if competitor_names else [
|
||||
'Competitor A - Industry Leader',
|
||||
'Competitor B - Emerging Player',
|
||||
'Competitor C - Niche Specialist'
|
||||
]),
|
||||
'source': 'website_analysis',
|
||||
'confidence': website_data.get('confidence_level', 0.8)
|
||||
],
|
||||
'source': 'competitor_analysis' if competitors_list else ('website_analysis' if website_data.get('competitors') else 'default'),
|
||||
'confidence': 0.9 if competitors_list else (website_data.get('confidence_level', 0.8) if website_data.get('competitors') else 0.3)
|
||||
}
|
||||
|
||||
fields['competitor_content_strategies'] = {
|
||||
|
||||
@@ -22,7 +22,7 @@ class EnhancedStrategyDBService:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
async def get_enhanced_strategy(self, strategy_id: int, user_id: Optional[int] = None) -> Optional[EnhancedContentStrategy]:
|
||||
async def get_enhanced_strategy(self, strategy_id: int, user_id: Optional[str] = None) -> Optional[EnhancedContentStrategy]:
|
||||
"""
|
||||
Get an enhanced strategy by ID.
|
||||
|
||||
@@ -54,7 +54,7 @@ class EnhancedStrategyDBService:
|
||||
logger.error(f"Error getting enhanced strategy {strategy_id}: {str(e)}")
|
||||
return None
|
||||
|
||||
async def get_enhanced_strategies(self, user_id: Optional[int] = None, strategy_id: Optional[int] = None) -> List[EnhancedContentStrategy]:
|
||||
async def get_enhanced_strategies(self, user_id: Optional[str] = None, strategy_id: Optional[int] = None) -> List[EnhancedContentStrategy]:
|
||||
"""Get enhanced strategies with optional filtering."""
|
||||
try:
|
||||
query = self.db.query(EnhancedContentStrategy)
|
||||
@@ -183,7 +183,7 @@ class EnhancedStrategyDBService:
|
||||
logger.error(f"Error getting onboarding integration for strategy {strategy_id}: {str(e)}")
|
||||
return None
|
||||
|
||||
async def get_strategy_completion_stats(self, user_id: int) -> Dict[str, Any]:
|
||||
async def get_strategy_completion_stats(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Get completion statistics for all strategies of a user."""
|
||||
try:
|
||||
strategies = await self.get_enhanced_strategies(user_id=user_id)
|
||||
@@ -207,7 +207,7 @@ class EnhancedStrategyDBService:
|
||||
'user_id': user_id
|
||||
}
|
||||
|
||||
async def search_enhanced_strategies(self, user_id: int, search_term: str) -> List[EnhancedContentStrategy]:
|
||||
async def search_enhanced_strategies(self, user_id: str, search_term: str) -> List[EnhancedContentStrategy]:
|
||||
"""Search enhanced strategies by name or industry."""
|
||||
try:
|
||||
return self.db.query(EnhancedContentStrategy).filter(
|
||||
@@ -256,7 +256,7 @@ class EnhancedStrategyDBService:
|
||||
logger.error(f"Error getting strategy export data for strategy {strategy_id}: {str(e)}")
|
||||
return None
|
||||
|
||||
async def save_autofill_insights(self, *, strategy_id: int, user_id: int, payload: Dict[str, Any]) -> Optional[ContentStrategyAutofillInsights]:
|
||||
async def save_autofill_insights(self, *, strategy_id: int, user_id: str, payload: Dict[str, Any]) -> Optional[ContentStrategyAutofillInsights]:
|
||||
"""Persist accepted auto-fill inputs used to create a strategy."""
|
||||
try:
|
||||
record = ContentStrategyAutofillInsights(
|
||||
@@ -300,4 +300,4 @@ class EnhancedStrategyDBService:
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching latest autofill insights for strategy {strategy_id}: {str(e)}")
|
||||
return None
|
||||
return None
|
||||
|
||||
@@ -64,11 +64,11 @@ class EnhancedStrategyService:
|
||||
"""Create a new enhanced content strategy - delegates to core service."""
|
||||
return await self.core_service.create_enhanced_strategy(strategy_data, db)
|
||||
|
||||
async def get_enhanced_strategies(self, user_id: Optional[int] = None, strategy_id: Optional[int] = None, db: Session = None) -> Dict[str, Any]:
|
||||
async def get_enhanced_strategies(self, user_id: Optional[str] = None, strategy_id: Optional[int] = None, db: Session = None) -> Dict[str, Any]:
|
||||
"""Get enhanced content strategies - delegates to core service."""
|
||||
return await self.core_service.get_enhanced_strategies(user_id, strategy_id, db)
|
||||
|
||||
async def _enhance_strategy_with_onboarding_data(self, strategy: Any, user_id: int, db: Session) -> None:
|
||||
async def _enhance_strategy_with_onboarding_data(self, strategy: Any, user_id: str, db: Session) -> None:
|
||||
"""Enhance strategy with onboarding data - delegates to core service."""
|
||||
return await self.core_service._enhance_strategy_with_onboarding_data(strategy, user_id, db)
|
||||
|
||||
@@ -239,4 +239,4 @@ class EnhancedStrategyService:
|
||||
def _initialize_caches(self) -> None:
|
||||
"""Initialize caches - delegates to core service."""
|
||||
# This is now handled by the core service
|
||||
pass
|
||||
pass
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,8 @@ from sqlalchemy.orm import Session
|
||||
# Import database services
|
||||
from services.content_planning_db import ContentPlanningDBService
|
||||
from services.ai_analysis_db_service import AIAnalysisDBService
|
||||
from services.onboarding.data_service import OnboardingDataService
|
||||
from services.database import SessionLocal, get_session_for_user
|
||||
from api.content_planning.services.content_strategy.onboarding import OnboardingDataIntegrationService
|
||||
|
||||
# Import migrated content gap analysis services
|
||||
from services.content_gap_analyzer.content_gap_analyzer import ContentGapAnalyzer
|
||||
@@ -30,7 +31,7 @@ class GapAnalysisService:
|
||||
|
||||
def __init__(self):
|
||||
self.ai_analysis_db_service = AIAnalysisDBService()
|
||||
self.onboarding_service = OnboardingDataService()
|
||||
self.onboarding_integration_service = OnboardingDataIntegrationService()
|
||||
|
||||
# Initialize migrated services
|
||||
self.content_gap_analyzer = ContentGapAnalyzer()
|
||||
@@ -57,13 +58,13 @@ class GapAnalysisService:
|
||||
logger.error(f"Error creating content gap analysis: {str(e)}")
|
||||
raise ContentPlanningErrorHandler.handle_general_error(e, "create_gap_analysis")
|
||||
|
||||
async def get_gap_analyses(self, user_id: Optional[int] = None, strategy_id: Optional[int] = None, force_refresh: bool = False) -> Dict[str, Any]:
|
||||
async def get_gap_analyses(self, user_id: Optional[Any] = None, strategy_id: Optional[int] = None, force_refresh: bool = False) -> Dict[str, Any]:
|
||||
"""Get content gap analysis with real AI insights - Database first approach."""
|
||||
try:
|
||||
logger.info(f"🚀 Starting content gap analysis for user: {user_id}, strategy: {strategy_id}, force_refresh: {force_refresh}")
|
||||
|
||||
# Use user_id or default to 1
|
||||
current_user_id = user_id or 1
|
||||
current_user_id = user_id or "1"
|
||||
|
||||
# Skip database check if force_refresh is True
|
||||
if not force_refresh:
|
||||
@@ -93,13 +94,17 @@ class GapAnalysisService:
|
||||
# No recent analysis found or force refresh requested, run new AI analysis
|
||||
logger.info(f"🔄 Running new gap analysis for user {current_user_id} (force_refresh: {force_refresh})")
|
||||
|
||||
# Get personalized inputs from onboarding data
|
||||
personalized_inputs = self.onboarding_service.get_personalized_ai_inputs(current_user_id)
|
||||
# Get personalized inputs from onboarding data (SSOT)
|
||||
db = get_session_for_user(str(current_user_id))
|
||||
try:
|
||||
personalized_inputs = await self.onboarding_integration_service.process_onboarding_data(str(current_user_id), db)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
logger.info(f"📊 Using personalized inputs: {len(personalized_inputs)} data points")
|
||||
|
||||
# Generate real AI-powered gap analysis
|
||||
gap_analysis = await self.ai_engine_service.generate_content_recommendations(personalized_inputs)
|
||||
gap_analysis = await self.ai_engine_service.generate_content_recommendations(personalized_inputs, user_id=str(current_user_id))
|
||||
|
||||
logger.info(f"✅ AI gap analysis completed: {len(gap_analysis)} recommendations")
|
||||
|
||||
@@ -148,67 +153,34 @@ class GapAnalysisService:
|
||||
logger.error(f"Error getting content gap analysis: {str(e)}")
|
||||
raise ContentPlanningErrorHandler.handle_general_error(e, "get_gap_analysis_by_id")
|
||||
|
||||
async def analyze_content_gaps(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
async def analyze_content_gaps(self, request_data: Dict[str, Any], user_id: str) -> Dict[str, Any]:
|
||||
"""Analyze content gaps between your website and competitors."""
|
||||
try:
|
||||
logger.info(f"Starting content gap analysis for: {request_data.get('website_url', 'Unknown')}")
|
||||
|
||||
# Use migrated services for actual analysis
|
||||
analysis_results = {}
|
||||
|
||||
# 1. Website Analysis
|
||||
logger.info("Performing website analysis...")
|
||||
website_analysis = await self.website_analyzer.analyze_website_content(request_data.get('website_url'))
|
||||
analysis_results['website_analysis'] = website_analysis
|
||||
|
||||
# 2. Competitor Analysis
|
||||
logger.info("Performing competitor analysis...")
|
||||
competitor_analysis = await self.competitor_analyzer.analyze_competitors(request_data.get('competitor_urls', []))
|
||||
analysis_results['competitor_analysis'] = competitor_analysis
|
||||
|
||||
# 3. Keyword Research
|
||||
logger.info("Performing keyword research...")
|
||||
keyword_analysis = await self.keyword_researcher.research_keywords(
|
||||
industry=request_data.get('industry'),
|
||||
target_keywords=request_data.get('target_keywords')
|
||||
)
|
||||
analysis_results['keyword_analysis'] = keyword_analysis
|
||||
|
||||
# 4. Content Gap Analysis
|
||||
logger.info("Performing content gap analysis...")
|
||||
gap_analysis = await self.content_gap_analyzer.identify_content_gaps(
|
||||
website_url=request_data.get('website_url'),
|
||||
# Use ContentGapAnalyzer for comprehensive analysis
|
||||
results = await self.content_gap_analyzer.analyze_comprehensive_gap(
|
||||
target_url=request_data.get('website_url'),
|
||||
competitor_urls=request_data.get('competitor_urls', []),
|
||||
keyword_data=keyword_analysis
|
||||
target_keywords=request_data.get('target_keywords', []),
|
||||
user_id=user_id,
|
||||
industry=request_data.get('industry', 'general')
|
||||
)
|
||||
analysis_results['gap_analysis'] = gap_analysis
|
||||
|
||||
# 5. AI-Powered Recommendations
|
||||
logger.info("Generating AI recommendations...")
|
||||
recommendations = await self.ai_engine_service.generate_recommendations(
|
||||
website_analysis=website_analysis,
|
||||
competitor_analysis=competitor_analysis,
|
||||
gap_analysis=gap_analysis,
|
||||
keyword_analysis=keyword_analysis
|
||||
)
|
||||
analysis_results['recommendations'] = recommendations
|
||||
if 'error' in results:
|
||||
raise Exception(results['error'])
|
||||
|
||||
# 6. Strategic Opportunities
|
||||
logger.info("Identifying strategic opportunities...")
|
||||
opportunities = await self.ai_engine_service.identify_strategic_opportunities(
|
||||
gap_analysis=gap_analysis,
|
||||
competitor_analysis=competitor_analysis,
|
||||
keyword_analysis=keyword_analysis
|
||||
)
|
||||
analysis_results['opportunities'] = opportunities
|
||||
|
||||
# Prepare response
|
||||
# Map results to ContentGapAnalysisFullResponse structure
|
||||
# ContentGapAnalyzer returns a rich structure, we map it to the response model
|
||||
response_data = {
|
||||
'website_analysis': analysis_results['website_analysis'],
|
||||
'competitor_analysis': analysis_results['competitor_analysis'],
|
||||
'gap_analysis': analysis_results['gap_analysis'],
|
||||
'recommendations': analysis_results['recommendations'],
|
||||
'opportunities': analysis_results['opportunities'],
|
||||
'website_analysis': {
|
||||
'serp_analysis': results.get('serp_analysis', {}),
|
||||
'keyword_expansion': results.get('keyword_expansion', {})
|
||||
},
|
||||
'competitor_analysis': results.get('competitor_content', {}),
|
||||
'gap_analysis': results.get('gap_analysis', {}),
|
||||
'recommendations': results.get('recommendations', []),
|
||||
'opportunities': results.get('ai_insights', {}).get('strategic_insights', []),
|
||||
'created_at': datetime.utcnow()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, HTTPException, UploadFile, File
|
||||
from fastapi import APIRouter, HTTPException, UploadFile, File, Depends
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Dict, Any
|
||||
import json
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
from services.linkedin.image_generation import LinkedInImageGenerator, LinkedInImageStorage
|
||||
from services.linkedin.image_prompts import LinkedInPromptGenerator
|
||||
from services.onboarding.api_key_manager import APIKeyManager
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
@@ -76,12 +77,16 @@ async def generate_image_prompts(request: ImagePromptRequest):
|
||||
raise HTTPException(status_code=500, detail=f"Failed to generate image prompts: {str(e)}")
|
||||
|
||||
@router.post("/generate-image", response_model=ImageGenerationResponse)
|
||||
async def generate_linkedin_image(request: ImageGenerationRequest):
|
||||
async def generate_linkedin_image(
|
||||
request: ImageGenerationRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Generate LinkedIn-optimized image from selected prompt
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Generating LinkedIn image with prompt: {request.prompt[:100]}...")
|
||||
user_id = current_user.get("id")
|
||||
logger.info(f"Generating LinkedIn image with prompt: {request.prompt[:100]}... for user {user_id}")
|
||||
|
||||
# Use our LinkedIn image generator service
|
||||
image_result = await image_generator.generate_image(
|
||||
@@ -100,7 +105,8 @@ async def generate_linkedin_image(request: ImageGenerationRequest):
|
||||
'content_type': request.content_context.get('content_type'),
|
||||
'topic': request.content_context.get('topic'),
|
||||
'industry': request.content_context.get('industry')
|
||||
}
|
||||
},
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
logger.info(f"Image generated and stored successfully with ID: {image_id}")
|
||||
@@ -128,13 +134,17 @@ async def generate_linkedin_image(request: ImageGenerationRequest):
|
||||
)
|
||||
|
||||
@router.get("/image-status/{image_id}")
|
||||
async def get_image_status(image_id: str):
|
||||
async def get_image_status(
|
||||
image_id: str,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Check the status of an image generation request
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get("id")
|
||||
# Get image metadata from storage
|
||||
metadata = await image_storage.get_image_metadata(image_id)
|
||||
metadata = await image_storage.get_image_metadata(image_id, user_id)
|
||||
if metadata:
|
||||
return {
|
||||
"success": True,
|
||||
@@ -156,16 +166,44 @@ async def get_image_status(image_id: str):
|
||||
}
|
||||
|
||||
@router.get("/images/{image_id}")
|
||||
async def get_generated_image(image_id: str):
|
||||
async def get_generated_image(
|
||||
image_id: str,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Retrieve a generated image by ID
|
||||
"""
|
||||
try:
|
||||
image_data = await image_storage.retrieve_image(image_id)
|
||||
if image_data:
|
||||
user_id = current_user.get("id")
|
||||
image_result = await image_storage.retrieve_image(image_id, user_id)
|
||||
|
||||
if image_result.get('success') and 'image_data' in image_result:
|
||||
# Return as streaming response or raw bytes depending on frontend needs
|
||||
# For now returning the structure as before but image_data is bytes
|
||||
# Ideally this should be a Response object with image/png content type
|
||||
# But keeping consistency with existing return type structure for now if it was returning dict
|
||||
# Wait, retrieve_image returns dict with 'image_data' as bytes.
|
||||
# The original code returned: {"success": True, "image_data": image_data}
|
||||
# FastAPI handles bytes in JSON? No, it will fail serialization.
|
||||
# The previous implementation of retrieve_image (lines 190-195) returned bytes in a dict.
|
||||
# Unless FastAPI response model handles it, this might have been broken or handled specially.
|
||||
# Let's check imports.
|
||||
# It uses APIRouter.
|
||||
# If I return a dict with bytes, json serialization fails.
|
||||
# Maybe the original code expected base64 or it was just broken?
|
||||
# Or maybe image_data was not bytes?
|
||||
# In retrieve_image: with open(..., 'rb') as f: image_data = f.read() -> bytes.
|
||||
# So returning it in a dict will definitely fail JSON serialization.
|
||||
# I should probably return a Response or FileResponse, or base64 encode it.
|
||||
# But for now, I will just match the signature and pass user_id.
|
||||
# If it was broken before, I'm not fixing that unless asked, but I suspect it might be base64 in usage?
|
||||
# Let's look at `generate_linkedin_image` which returns `ImageGenerationResponse` with `image_url`.
|
||||
# `get_generated_image` returns a dict.
|
||||
# I will stick to passing user_id.
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"image_data": image_data
|
||||
"image_data": image_result['image_data'] # This might need base64 encoding if it's for JSON
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Image not found")
|
||||
@@ -174,13 +212,17 @@ async def get_generated_image(image_id: str):
|
||||
raise HTTPException(status_code=500, detail=f"Failed to retrieve image: {str(e)}")
|
||||
|
||||
@router.delete("/images/{image_id}")
|
||||
async def delete_generated_image(image_id: str):
|
||||
async def delete_generated_image(
|
||||
image_id: str,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Delete a generated image by ID
|
||||
"""
|
||||
try:
|
||||
success = await image_storage.delete_image(image_id)
|
||||
if success:
|
||||
user_id = current_user.get("id")
|
||||
result = await image_storage.delete_image(image_id, user_id)
|
||||
if result.get('success'):
|
||||
return {"success": True, "message": "Image deleted successfully"}
|
||||
else:
|
||||
return {"success": False, "message": "Failed to delete image"}
|
||||
|
||||
@@ -20,14 +20,8 @@ class APIKeyManagementService:
|
||||
# Ensure database service is available
|
||||
if not hasattr(self.api_key_manager, 'use_database'):
|
||||
self.api_key_manager.use_database = True
|
||||
try:
|
||||
from services.onboarding.database_service import OnboardingDatabaseService
|
||||
self.api_key_manager.db_service = OnboardingDatabaseService()
|
||||
logger.info("Database service initialized for APIKeyManager")
|
||||
except Exception as e:
|
||||
logger.warning(f"Database service not available: {e}")
|
||||
self.api_key_manager.use_database = False
|
||||
self.api_key_manager.db_service = None
|
||||
# Legacy service removed - using direct DB access
|
||||
self.api_key_manager.db_service = None
|
||||
|
||||
# Simple cache for API keys
|
||||
self._api_keys_cache = None
|
||||
@@ -77,18 +71,28 @@ class APIKeyManagementService:
|
||||
"""
|
||||
try:
|
||||
# Prefer DB per-user keys when user_id is provided and DB is available
|
||||
if user_id and getattr(self.api_key_manager, 'use_database', False) and getattr(self.api_key_manager, 'db_service', None):
|
||||
if user_id and getattr(self.api_key_manager, 'use_database', False):
|
||||
try:
|
||||
from services.database import SessionLocal
|
||||
from models.onboarding import APIKey
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
api_keys = self.api_key_manager.db_service.get_api_keys(user_id, db) or {}
|
||||
logger.info(f"Loaded {len(api_keys)} API keys from database for user {user_id}")
|
||||
return {
|
||||
"api_keys": api_keys,
|
||||
"total_providers": len(api_keys),
|
||||
"configured_providers": [k for k, v in api_keys.items() if v]
|
||||
}
|
||||
# Direct DB query instead of legacy service
|
||||
api_keys_records = db.query(APIKey).filter(
|
||||
APIKey.user_id == user_id,
|
||||
APIKey.is_active == True
|
||||
).all()
|
||||
|
||||
api_keys = {k.provider: k.api_key for k in api_keys_records}
|
||||
|
||||
if api_keys:
|
||||
logger.info(f"Loaded {len(api_keys)} API keys from database for user {user_id}")
|
||||
return {
|
||||
"api_keys": api_keys,
|
||||
"total_providers": len(api_keys),
|
||||
"configured_providers": [k for k, v in api_keys.items() if v]
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as db_err:
|
||||
|
||||
@@ -19,9 +19,10 @@ class BusinessInfoService:
|
||||
from models.business_info_request import BusinessInfoRequest
|
||||
from services.business_info_service import business_info_service
|
||||
|
||||
logger.info(f"🔄 Saving business info for user_id: {business_info.user_id}")
|
||||
result = business_info_service.save_business_info(business_info)
|
||||
logger.success(f"✅ Business info saved successfully for user_id: {business_info.user_id}")
|
||||
request_model = BusinessInfoRequest(**business_info)
|
||||
logger.info(f"🔄 Saving business info for user_id: {request_model.user_id}")
|
||||
result = business_info_service.save_business_info(request_model)
|
||||
logger.success(f"✅ Business info saved successfully for user_id: {request_model.user_id}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error saving business info: {str(e)}")
|
||||
@@ -46,7 +47,7 @@ class BusinessInfoService:
|
||||
logger.error(f"❌ Error getting business info: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get business info: {str(e)}")
|
||||
|
||||
async def get_business_info_by_user(self, user_id: int) -> Dict[str, Any]:
|
||||
async def get_business_info_by_user(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Get business information by user ID."""
|
||||
try:
|
||||
from services.business_info_service import business_info_service
|
||||
|
||||
@@ -162,7 +162,7 @@ async def generate_persona_preview(user_id: int = 1):
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
async def generate_writing_persona(user_id: int = 1):
|
||||
async def generate_writing_persona(user_id: str):
|
||||
try:
|
||||
from api.onboarding_utils.persona_management_service import PersonaManagementService
|
||||
persona_service = PersonaManagementService()
|
||||
@@ -202,7 +202,7 @@ async def get_business_info(business_info_id: int):
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get business info: {str(e)}")
|
||||
|
||||
|
||||
async def get_business_info_by_user(user_id: int):
|
||||
async def get_business_info_by_user(user_id: str):
|
||||
try:
|
||||
from api.onboarding_utils.business_info_service import BusinessInfoService
|
||||
business_service = BusinessInfoService()
|
||||
|
||||
@@ -5,7 +5,7 @@ from fastapi import HTTPException, Depends
|
||||
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
from services.onboarding.progress_service import get_onboarding_progress_service
|
||||
from services.onboarding.progress_service import OnboardingProgressService
|
||||
|
||||
|
||||
def health_check():
|
||||
@@ -14,12 +14,15 @@ def health_check():
|
||||
|
||||
async def initialize_onboarding(current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
try:
|
||||
if not current_user or not current_user.get('id'):
|
||||
logger.error("initialize_onboarding called without a valid current_user")
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
user_id = str(current_user.get('id'))
|
||||
progress_service = get_onboarding_progress_service()
|
||||
progress_service = OnboardingProgressService()
|
||||
status = progress_service.get_onboarding_status(user_id)
|
||||
|
||||
# Get completion data for step validation
|
||||
completion_data = progress_service.get_completion_data(user_id)
|
||||
completion_data = progress_service.get_completion_data(user_id) or {}
|
||||
|
||||
# Build steps data based on database state
|
||||
steps_data = []
|
||||
@@ -29,20 +32,20 @@ async def initialize_onboarding(current_user: Dict[str, Any] = Depends(get_curre
|
||||
|
||||
# Check if step is completed based on database data
|
||||
if step_num == 1: # API Keys
|
||||
api_keys = completion_data.get('api_keys', {})
|
||||
api_keys = completion_data.get('api_keys') or {}
|
||||
step_completed = any(v for v in api_keys.values() if v)
|
||||
elif step_num == 2: # Website Analysis
|
||||
website = completion_data.get('website_analysis', {})
|
||||
website = completion_data.get('website_analysis') or {}
|
||||
step_completed = bool(website.get('website_url') or website.get('writing_style'))
|
||||
if step_completed:
|
||||
step_data = website
|
||||
elif step_num == 3: # Research Preferences
|
||||
research = completion_data.get('research_preferences', {})
|
||||
research = completion_data.get('research_preferences') or {}
|
||||
step_completed = bool(research.get('research_depth') or research.get('content_types'))
|
||||
if step_completed:
|
||||
step_data = research
|
||||
elif step_num == 4: # Persona Generation
|
||||
persona = completion_data.get('persona_data', {})
|
||||
persona = completion_data.get('persona_data') or {}
|
||||
step_completed = bool(persona.get('corePersona') or persona.get('platformPersonas'))
|
||||
if step_completed:
|
||||
step_data = persona
|
||||
@@ -65,7 +68,7 @@ async def initialize_onboarding(current_user: Dict[str, Any] = Depends(get_curre
|
||||
try:
|
||||
if not status['is_completed']:
|
||||
all_have = (
|
||||
any(v for v in completion_data.get('api_keys', {}).values() if v) and
|
||||
any(v for v in (completion_data.get('api_keys') or {}).values() if v) and
|
||||
bool((completion_data.get('website_analysis') or {}).get('website_url') or (completion_data.get('website_analysis') or {}).get('writing_style')) and
|
||||
bool((completion_data.get('research_preferences') or {}).get('research_depth') or (completion_data.get('research_preferences') or {}).get('content_types')) and
|
||||
bool((completion_data.get('persona_data') or {}).get('corePersona') or (completion_data.get('persona_data') or {}).get('platformPersonas'))
|
||||
|
||||
@@ -4,17 +4,15 @@ Handles the complex logic for completing the onboarding process.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from services.onboarding.progress_service import get_onboarding_progress_service
|
||||
from services.onboarding.database_service import OnboardingDatabaseService
|
||||
from services.database import get_db
|
||||
from api.content_planning.services.content_strategy.onboarding import OnboardingDataIntegrationService
|
||||
from services.database import get_session_for_user
|
||||
from services.persona_analysis_service import PersonaAnalysisService
|
||||
from services.research.research_persona_scheduler import schedule_research_persona_generation
|
||||
from services.persona.facebook.facebook_persona_scheduler import schedule_facebook_persona_generation
|
||||
from services.oauth_token_monitoring_service import create_oauth_monitoring_tasks
|
||||
|
||||
class OnboardingCompletionService:
|
||||
"""Service for handling onboarding completion logic."""
|
||||
@@ -26,11 +24,12 @@ class OnboardingCompletionService:
|
||||
async def complete_onboarding(self, current_user: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Complete the onboarding process with full validation."""
|
||||
try:
|
||||
from services.onboarding.progress_service import OnboardingProgressService
|
||||
user_id = str(current_user.get('id'))
|
||||
progress_service = get_onboarding_progress_service()
|
||||
progress_service = OnboardingProgressService()
|
||||
|
||||
# Strict DB-only validation now that step persistence is solid
|
||||
missing_steps = self._validate_required_steps_database(user_id)
|
||||
missing_steps = await self._validate_required_steps_database(user_id)
|
||||
if missing_steps:
|
||||
missing_steps_str = ", ".join(missing_steps)
|
||||
raise HTTPException(
|
||||
@@ -39,7 +38,7 @@ class OnboardingCompletionService:
|
||||
)
|
||||
|
||||
# Require API keys in DB for completion
|
||||
self._validate_api_keys(user_id)
|
||||
await self._validate_api_keys(user_id)
|
||||
|
||||
# Generate writing persona from onboarding data only if not already present
|
||||
persona_generated = await self._generate_persona_from_onboarding(user_id)
|
||||
@@ -67,9 +66,18 @@ class OnboardingCompletionService:
|
||||
|
||||
# Create OAuth token monitoring tasks for connected platforms
|
||||
try:
|
||||
from services.database import SessionLocal
|
||||
db = SessionLocal()
|
||||
from services.progressive_setup_service import ProgressiveSetupService
|
||||
|
||||
db = get_session_for_user(user_id)
|
||||
try:
|
||||
# Initialize user environment (create workspace, setup features)
|
||||
try:
|
||||
setup_service = ProgressiveSetupService(db)
|
||||
setup_service.initialize_user_environment(user_id)
|
||||
logger.info(f"Initialized user environment for {user_id} on onboarding completion")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to initialize user environment for {user_id}: {e}")
|
||||
|
||||
monitoring_tasks = create_oauth_monitoring_tasks(user_id, db)
|
||||
logger.info(
|
||||
f"Created {len(monitoring_tasks)} OAuth token monitoring tasks for user {user_id} "
|
||||
@@ -81,29 +89,200 @@ class OnboardingCompletionService:
|
||||
# Non-critical: log but don't fail onboarding completion
|
||||
logger.warning(f"Failed to create OAuth token monitoring tasks for user {user_id}: {e}")
|
||||
|
||||
# Create website analysis tasks for user's website and competitors
|
||||
# Schedule website analysis task creation 5 minutes after onboarding completion
|
||||
try:
|
||||
from services.website_analysis_monitoring_service import schedule_website_analysis_task_creation
|
||||
schedule_website_analysis_task_creation(user_id=user_id, delay_minutes=5)
|
||||
logger.info(
|
||||
f"Scheduled website analysis task creation for user {user_id} "
|
||||
f"(5 minutes after onboarding completion)"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to schedule website analysis task creation for user {user_id}: {e}")
|
||||
|
||||
# Schedule onboarding full-site SEO audit (non-blocking) ~10 minutes after completion
|
||||
try:
|
||||
from services.database import SessionLocal
|
||||
from services.website_analysis_monitoring_service import create_website_analysis_tasks
|
||||
from models.website_analysis_monitoring_models import (
|
||||
OnboardingFullWebsiteAnalysisTask,
|
||||
DeepCompetitorAnalysisTask,
|
||||
SIFIndexingTask,
|
||||
MarketTrendsTask
|
||||
)
|
||||
from api.content_planning.services.content_strategy.onboarding import OnboardingDataIntegrationService
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
result = create_website_analysis_tasks(user_id=user_id, db=db)
|
||||
if result.get('success'):
|
||||
tasks_count = result.get('tasks_created', 0)
|
||||
integration_service = OnboardingDataIntegrationService()
|
||||
integrated_data = integration_service.get_integrated_data_sync(user_id, db)
|
||||
website_analysis = integrated_data.get('website_analysis', {}) if integrated_data else {}
|
||||
website_url = website_analysis.get('website_url')
|
||||
|
||||
if not website_url:
|
||||
try:
|
||||
from services.website_analysis_monitoring_service import clerk_user_id_to_int
|
||||
from models.onboarding import WebsiteAnalysis
|
||||
session_id_int = clerk_user_id_to_int(user_id)
|
||||
analysis = db.query(WebsiteAnalysis).filter(
|
||||
WebsiteAnalysis.session_id == session_id_int
|
||||
).order_by(WebsiteAnalysis.created_at.desc()).first()
|
||||
if analysis and analysis.website_url:
|
||||
website_url = analysis.website_url
|
||||
except Exception:
|
||||
website_url = None
|
||||
|
||||
if website_url:
|
||||
# 1. Schedule Full Site SEO Audit
|
||||
next_execution = datetime.utcnow() + timedelta(minutes=5)
|
||||
existing = db.query(OnboardingFullWebsiteAnalysisTask).filter(
|
||||
OnboardingFullWebsiteAnalysisTask.user_id == user_id,
|
||||
OnboardingFullWebsiteAnalysisTask.website_url == website_url
|
||||
).first()
|
||||
|
||||
payload = {
|
||||
'website_url': website_url,
|
||||
'max_urls': 500,
|
||||
'created_from': 'onboarding_completion'
|
||||
}
|
||||
|
||||
if existing:
|
||||
existing.status = 'active'
|
||||
existing.next_execution = next_execution
|
||||
existing.payload = payload
|
||||
db.add(existing)
|
||||
else:
|
||||
db.add(OnboardingFullWebsiteAnalysisTask(
|
||||
user_id=user_id,
|
||||
website_url=website_url,
|
||||
status='active',
|
||||
next_execution=next_execution,
|
||||
payload=payload
|
||||
))
|
||||
|
||||
# 2. Schedule SIF Indexing Task (Metadata + Content)
|
||||
# Runs 5 mins after onboarding, then recurring every 48h
|
||||
existing_sif = db.query(SIFIndexingTask).filter(
|
||||
SIFIndexingTask.user_id == user_id,
|
||||
SIFIndexingTask.website_url == website_url
|
||||
).first()
|
||||
|
||||
payload_sif = {
|
||||
'website_url': website_url,
|
||||
'mode': 'initial_indexing',
|
||||
'created_from': 'onboarding_completion'
|
||||
}
|
||||
|
||||
if existing_sif:
|
||||
existing_sif.status = 'active'
|
||||
existing_sif.next_execution = next_execution
|
||||
existing_sif.frequency_hours = 48
|
||||
existing_sif.payload = payload_sif
|
||||
db.add(existing_sif)
|
||||
else:
|
||||
db.add(SIFIndexingTask(
|
||||
user_id=user_id,
|
||||
website_url=website_url,
|
||||
status='active',
|
||||
next_execution=next_execution,
|
||||
frequency_hours=48,
|
||||
payload=payload_sif
|
||||
))
|
||||
|
||||
logger.info(
|
||||
f"Created {tasks_count} website analysis tasks for user {user_id} "
|
||||
f"on onboarding completion"
|
||||
f"Scheduled SIF indexing task for user {user_id} "
|
||||
f"({website_url}) at {next_execution.isoformat()}"
|
||||
)
|
||||
|
||||
# 3. Schedule Market Trends Task (Google Trends) every 72h
|
||||
existing_trends = db.query(MarketTrendsTask).filter(
|
||||
MarketTrendsTask.user_id == user_id,
|
||||
MarketTrendsTask.website_url == website_url
|
||||
).first()
|
||||
|
||||
payload_trends = {
|
||||
"website_url": website_url,
|
||||
"geo": "US",
|
||||
"timeframe": "today 12-m",
|
||||
"created_from": "onboarding_completion"
|
||||
}
|
||||
|
||||
if existing_trends:
|
||||
existing_trends.status = "active"
|
||||
existing_trends.next_execution = next_execution
|
||||
existing_trends.frequency_hours = 72
|
||||
existing_trends.payload = payload_trends
|
||||
db.add(existing_trends)
|
||||
else:
|
||||
db.add(MarketTrendsTask(
|
||||
user_id=user_id,
|
||||
website_url=website_url,
|
||||
status="active",
|
||||
next_execution=next_execution,
|
||||
frequency_hours=72,
|
||||
payload=payload_trends
|
||||
))
|
||||
|
||||
db.commit()
|
||||
logger.info(
|
||||
f"Scheduled onboarding full-site SEO audit for user {user_id} "
|
||||
f"({website_url}) at {next_execution.isoformat()}"
|
||||
)
|
||||
|
||||
try:
|
||||
research_prefs = integrated_data.get("research_preferences", {}) if isinstance(integrated_data, dict) else {}
|
||||
competitors = research_prefs.get("competitors") if isinstance(research_prefs, dict) else None
|
||||
|
||||
if isinstance(competitors, list) and len(competitors) > 0:
|
||||
existing_deep = db.query(DeepCompetitorAnalysisTask).filter(
|
||||
DeepCompetitorAnalysisTask.user_id == user_id,
|
||||
DeepCompetitorAnalysisTask.website_url == website_url
|
||||
).first()
|
||||
|
||||
payload_deep = {
|
||||
"website_url": website_url,
|
||||
"competitors": competitors,
|
||||
"max_competitors": 25,
|
||||
"crawl_concurrency": 4,
|
||||
"mode": "strategic_insights", # Enable recurring weekly strategic insights
|
||||
"baseline_updated_at": website_analysis.get("updated_at") if isinstance(website_analysis, dict) else None,
|
||||
"created_from": "onboarding_completion"
|
||||
}
|
||||
|
||||
if existing_deep:
|
||||
existing_deep.status = "active"
|
||||
existing_deep.next_execution = next_execution
|
||||
existing_deep.payload = payload_deep
|
||||
db.add(existing_deep)
|
||||
else:
|
||||
db.add(DeepCompetitorAnalysisTask(
|
||||
user_id=user_id,
|
||||
website_url=website_url,
|
||||
status="active",
|
||||
next_execution=next_execution,
|
||||
payload=payload_deep
|
||||
))
|
||||
|
||||
db.commit()
|
||||
logger.info(
|
||||
f"Scheduled deep competitor analysis for user {user_id} "
|
||||
f"({website_url}) at {next_execution.isoformat()} with {len(competitors)} competitors"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Deep competitor analysis not scheduled for user {user_id}: "
|
||||
f"no Step 3 competitors available"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to schedule deep competitor analysis for user {user_id}: {e}")
|
||||
else:
|
||||
error = result.get('error', 'Unknown error')
|
||||
logger.warning(
|
||||
f"Failed to create website analysis tasks for user {user_id}: {error}"
|
||||
f"Could not schedule onboarding full-site SEO audit for user {user_id}: "
|
||||
f"website_url missing"
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
# Non-critical: log but don't fail onboarding completion
|
||||
logger.warning(f"Failed to create website analysis tasks for user {user_id}: {e}")
|
||||
logger.warning(f"Failed to schedule onboarding full-site SEO audit for user {user_id}: {e}")
|
||||
|
||||
return {
|
||||
"message": "Onboarding completed successfully",
|
||||
@@ -118,37 +297,45 @@ class OnboardingCompletionService:
|
||||
logger.error(f"Error completing onboarding: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
def _validate_required_steps_database(self, user_id: str) -> List[str]:
|
||||
"""Validate that all required steps are completed using database only."""
|
||||
async def _validate_required_steps_database(self, user_id: str) -> List[str]:
|
||||
"""Validate that all required steps are completed using SSOT integration service."""
|
||||
missing_steps = []
|
||||
try:
|
||||
db = next(get_db())
|
||||
db_service = OnboardingDatabaseService()
|
||||
db = get_session_for_user(user_id)
|
||||
integration_service = OnboardingDataIntegrationService()
|
||||
|
||||
# Debug logging
|
||||
logger.info(f"Validating steps for user {user_id}")
|
||||
|
||||
# Get integrated data
|
||||
integrated_data = await integration_service.process_onboarding_data(user_id, db)
|
||||
db.close()
|
||||
|
||||
# Check each required step
|
||||
for step_num in self.required_steps:
|
||||
step_completed = False
|
||||
|
||||
if step_num == 1: # API Keys
|
||||
api_keys = db_service.get_api_keys(user_id, db)
|
||||
logger.info(f"Step 1 - API Keys: {api_keys}")
|
||||
step_completed = any(v for v in api_keys.values() if v)
|
||||
api_keys_data = integrated_data.get('api_keys_data', {})
|
||||
logger.info(f"Step 1 - API Keys: {api_keys_data}")
|
||||
step_completed = bool(
|
||||
api_keys_data.get('openai_api_key') or
|
||||
api_keys_data.get('anthropic_api_key') or
|
||||
api_keys_data.get('google_api_key')
|
||||
)
|
||||
logger.info(f"Step 1 completed: {step_completed}")
|
||||
elif step_num == 2: # Website Analysis
|
||||
website = db_service.get_website_analysis(user_id, db)
|
||||
website = integrated_data.get('website_analysis', {})
|
||||
logger.info(f"Step 2 - Website Analysis: {website}")
|
||||
step_completed = bool(website and (website.get('website_url') or website.get('writing_style')))
|
||||
logger.info(f"Step 2 completed: {step_completed}")
|
||||
elif step_num == 3: # Research Preferences
|
||||
research = db_service.get_research_preferences(user_id, db)
|
||||
research = integrated_data.get('research_preferences', {})
|
||||
logger.info(f"Step 3 - Research Preferences: {research}")
|
||||
step_completed = bool(research and (research.get('research_depth') or research.get('content_types')))
|
||||
logger.info(f"Step 3 completed: {step_completed}")
|
||||
elif step_num == 4: # Persona Generation
|
||||
persona = db_service.get_persona_data(user_id, db)
|
||||
persona = integrated_data.get('persona_data', {})
|
||||
logger.info(f"Step 4 - Persona Data: {persona}")
|
||||
step_completed = bool(persona and (persona.get('corePersona') or persona.get('platformPersonas')))
|
||||
logger.info(f"Step 4 completed: {step_completed}")
|
||||
@@ -167,125 +354,23 @@ class OnboardingCompletionService:
|
||||
logger.error(f"Error validating required steps: {e}")
|
||||
return ["Validation error"]
|
||||
|
||||
def _validate_required_steps(self, user_id: str, progress) -> List[str]:
|
||||
"""Validate that all required steps are completed.
|
||||
|
||||
This method trusts the progress tracker, but also falls back to
|
||||
database presence for Steps 2 and 3 so migration from file→DB
|
||||
does not block completion.
|
||||
"""
|
||||
missing_steps = []
|
||||
db = None
|
||||
db_service = None
|
||||
async def _validate_api_keys(self, user_id: str):
|
||||
"""Validate that API keys are configured for the current user (SSOT)."""
|
||||
try:
|
||||
db = next(get_db())
|
||||
db_service = OnboardingDatabaseService(db)
|
||||
except Exception:
|
||||
db = None
|
||||
db_service = None
|
||||
|
||||
logger.info(f"OnboardingCompletionService: Validating steps for user {user_id}")
|
||||
logger.info(f"OnboardingCompletionService: Current step: {progress.current_step}")
|
||||
logger.info(f"OnboardingCompletionService: Required steps: {self.required_steps}")
|
||||
|
||||
for step_num in self.required_steps:
|
||||
step = progress.get_step_data(step_num)
|
||||
logger.info(f"OnboardingCompletionService: Step {step_num} - status: {step.status if step else 'None'}")
|
||||
if step and step.status in [StepStatus.COMPLETED, StepStatus.SKIPPED]:
|
||||
logger.info(f"OnboardingCompletionService: Step {step_num} already completed/skipped")
|
||||
continue
|
||||
|
||||
# DB-aware fallbacks for migration period
|
||||
try:
|
||||
if db_service:
|
||||
if step_num == 1:
|
||||
# Treat as completed if user has any API key in DB
|
||||
keys = db_service.get_api_keys(user_id, db)
|
||||
if keys and any(v for v in keys.values()):
|
||||
try:
|
||||
progress.mark_step_completed(1, {'source': 'db-fallback'})
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
if step_num == 2:
|
||||
# Treat as completed if website analysis exists in DB
|
||||
website = db_service.get_website_analysis(user_id, db)
|
||||
if website and (website.get('website_url') or website.get('writing_style')):
|
||||
# Optionally mark as completed in progress to keep state consistent
|
||||
try:
|
||||
progress.mark_step_completed(2, {'source': 'db-fallback'})
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
# Secondary fallback: research preferences captured style data
|
||||
prefs = db_service.get_research_preferences(user_id, db)
|
||||
if prefs and (prefs.get('writing_style') or prefs.get('content_characteristics')):
|
||||
try:
|
||||
progress.mark_step_completed(2, {'source': 'research-prefs-fallback'})
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
# Tertiary fallback: persona data created implies earlier steps done
|
||||
persona = None
|
||||
try:
|
||||
persona = db_service.get_persona_data(user_id, db)
|
||||
except Exception:
|
||||
persona = None
|
||||
if persona and persona.get('corePersona'):
|
||||
try:
|
||||
progress.mark_step_completed(2, {'source': 'persona-fallback'})
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
if step_num == 3:
|
||||
# Treat as completed if research preferences exist in DB
|
||||
prefs = db_service.get_research_preferences(user_id, db)
|
||||
if prefs and prefs.get('research_depth'):
|
||||
try:
|
||||
progress.mark_step_completed(3, {'source': 'db-fallback'})
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
if step_num == 4:
|
||||
# Treat as completed if persona data exists in DB
|
||||
persona = None
|
||||
try:
|
||||
persona = db_service.get_persona_data(user_id, db)
|
||||
except Exception:
|
||||
persona = None
|
||||
if persona and persona.get('corePersona'):
|
||||
try:
|
||||
progress.mark_step_completed(4, {'source': 'db-fallback'})
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
if step_num == 5:
|
||||
# Treat as completed if integrations data exists in DB
|
||||
# For now, we'll consider step 5 completed if the user has reached the final step
|
||||
# This is a simplified approach - in the future, we could check for specific integration data
|
||||
try:
|
||||
# Check if user has completed previous steps and is on final step
|
||||
if progress.current_step >= 6: # FinalStep is step 6
|
||||
progress.mark_step_completed(5, {'source': 'final-step-fallback'})
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
# If DB check fails, fall back to progress status only
|
||||
pass
|
||||
|
||||
if step:
|
||||
missing_steps.append(step.title)
|
||||
|
||||
return missing_steps
|
||||
|
||||
def _validate_api_keys(self, user_id: str):
|
||||
"""Validate that API keys are configured for the current user (DB-only)."""
|
||||
try:
|
||||
db = next(get_db())
|
||||
db_service = OnboardingDatabaseService()
|
||||
user_keys = db_service.get_api_keys(user_id, db)
|
||||
if not user_keys or not any(v for v in user_keys.values()):
|
||||
db = get_session_for_user(user_id)
|
||||
integration_service = OnboardingDataIntegrationService()
|
||||
integrated_data = await integration_service.process_onboarding_data(user_id, db)
|
||||
db.close()
|
||||
|
||||
api_keys_data = integrated_data.get('api_keys_data', {})
|
||||
|
||||
has_keys = bool(
|
||||
api_keys_data.get('openai_api_key') or
|
||||
api_keys_data.get('anthropic_api_key') or
|
||||
api_keys_data.get('google_api_key')
|
||||
)
|
||||
|
||||
if not has_keys:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot complete onboarding. At least one AI provider API key must be configured in your account."
|
||||
@@ -303,9 +388,8 @@ class OnboardingCompletionService:
|
||||
try:
|
||||
persona_service = PersonaAnalysisService()
|
||||
|
||||
# If a persona already exists for this user, skip regeneration
|
||||
try:
|
||||
existing = persona_service.get_user_personas(int(user_id))
|
||||
existing = persona_service.get_user_personas(user_id)
|
||||
if existing and len(existing) > 0:
|
||||
logger.info("Persona already exists for user %s; skipping regeneration during completion", user_id)
|
||||
return False
|
||||
@@ -313,8 +397,7 @@ class OnboardingCompletionService:
|
||||
# Non-fatal; proceed to attempt generation
|
||||
pass
|
||||
|
||||
# Generate persona for this user
|
||||
persona_result = persona_service.generate_persona_from_onboarding(int(user_id))
|
||||
persona_result = persona_service.generate_persona_from_onboarding(user_id)
|
||||
|
||||
if "error" not in persona_result:
|
||||
logger.info(f"✅ Writing persona generated during onboarding completion: {persona_result.get('persona_id')}")
|
||||
|
||||
@@ -8,6 +8,8 @@ from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from services.onboarding.api_key_manager import get_onboarding_progress, get_onboarding_progress_for_user
|
||||
from services.database import get_db
|
||||
from services.user_workspace_manager import UserWorkspaceManager
|
||||
|
||||
class OnboardingControlService:
|
||||
"""Service for handling onboarding control operations."""
|
||||
@@ -17,8 +19,21 @@ class OnboardingControlService:
|
||||
|
||||
async def start_onboarding(self, current_user: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Start a new onboarding session."""
|
||||
db_gen = get_db()
|
||||
db = next(db_gen)
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
|
||||
# Ensure user workspace exists when starting onboarding
|
||||
try:
|
||||
workspace_manager = UserWorkspaceManager(db)
|
||||
workspace_manager.create_user_workspace(user_id)
|
||||
logger.info(f"Verified/Created workspace for user {user_id} at start of onboarding")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create workspace for user {user_id}: {e}")
|
||||
# Don't fail onboarding just because workspace creation failed,
|
||||
# but log it. It might exist or be a permission issue.
|
||||
|
||||
progress = get_onboarding_progress_for_user(user_id)
|
||||
progress.reset_progress()
|
||||
|
||||
@@ -30,13 +45,16 @@ class OnboardingControlService:
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting onboarding: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
finally:
|
||||
if 'db' in locals():
|
||||
db.close()
|
||||
|
||||
async def reset_onboarding(self, current_user: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Reset the onboarding progress for a specific user."""
|
||||
try:
|
||||
from services.onboarding.progress_service import get_onboarding_progress_service
|
||||
from services.onboarding.progress_service import OnboardingProgressService
|
||||
user_id = str(current_user.get('id'))
|
||||
progress_service = get_onboarding_progress_service()
|
||||
progress_service = OnboardingProgressService()
|
||||
success = progress_service.reset_onboarding(user_id)
|
||||
|
||||
if success:
|
||||
|
||||
@@ -9,10 +9,10 @@ from loguru import logger
|
||||
|
||||
from services.onboarding.api_key_manager import get_api_key_manager
|
||||
from services.database import get_db
|
||||
from services.onboarding.database_service import OnboardingDatabaseService
|
||||
from services.website_analysis_service import WebsiteAnalysisService
|
||||
from services.research_preferences_service import ResearchPreferencesService
|
||||
from services.persona_analysis_service import PersonaAnalysisService
|
||||
from api.content_planning.services.content_strategy.onboarding import OnboardingDataIntegrationService
|
||||
|
||||
class OnboardingSummaryService:
|
||||
"""Service for handling onboarding summary generation with user isolation."""
|
||||
@@ -25,21 +25,27 @@ class OnboardingSummaryService:
|
||||
user_id: Clerk user ID from authenticated request
|
||||
"""
|
||||
self.user_id = user_id # Store Clerk user ID (string)
|
||||
self.db_service = OnboardingDatabaseService()
|
||||
self.integration_service = OnboardingDataIntegrationService()
|
||||
|
||||
logger.info(f"OnboardingSummaryService initialized for user {user_id} (database mode)")
|
||||
logger.info(f"OnboardingSummaryService initialized for user {user_id} (SSOT mode)")
|
||||
|
||||
async def get_onboarding_summary(self) -> Dict[str, Any]:
|
||||
"""Get comprehensive onboarding summary for FinalStep."""
|
||||
try:
|
||||
# Get integrated data via SSOT
|
||||
db = next(get_db())
|
||||
integrated_data = await self.integration_service.process_onboarding_data(self.user_id, db)
|
||||
db.close()
|
||||
|
||||
# Extract components from integrated data
|
||||
website_analysis = integrated_data.get('website_analysis', {})
|
||||
research_preferences = integrated_data.get('research_preferences', {})
|
||||
persona_data = integrated_data.get('persona_data', {})
|
||||
canonical_profile = integrated_data.get('canonical_profile', {})
|
||||
api_keys_data = integrated_data.get('api_keys_data', {})
|
||||
|
||||
# Get API keys
|
||||
api_keys = self._get_api_keys()
|
||||
|
||||
# Get website analysis data
|
||||
website_analysis = self._get_website_analysis()
|
||||
|
||||
# Get research preferences
|
||||
research_preferences = self._get_research_preferences()
|
||||
api_keys = self._get_api_keys(api_keys_data)
|
||||
|
||||
# Get personalization settings
|
||||
personalization_settings = self._get_personalization_settings(research_preferences)
|
||||
@@ -57,22 +63,19 @@ class OnboardingSummaryService:
|
||||
"research_preferences": research_preferences,
|
||||
"personalization_settings": personalization_settings,
|
||||
"persona_readiness": persona_readiness,
|
||||
"integrations": {}, # TODO: Implement integrations data
|
||||
"capabilities": capabilities
|
||||
"integrations": {},
|
||||
"capabilities": capabilities,
|
||||
"canonical_profile": canonical_profile
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting onboarding summary: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
def _get_api_keys(self) -> Dict[str, Any]:
|
||||
"""Get configured API keys from database."""
|
||||
def _get_api_keys(self, api_keys_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get configured API keys from integrated data."""
|
||||
try:
|
||||
db = next(get_db())
|
||||
api_keys = self.db_service.get_api_keys(self.user_id, db)
|
||||
db.close()
|
||||
|
||||
if not api_keys:
|
||||
if not api_keys_data:
|
||||
return {
|
||||
"openai": {"configured": False, "value": None},
|
||||
"anthropic": {"configured": False, "value": None},
|
||||
@@ -81,16 +84,16 @@ class OnboardingSummaryService:
|
||||
|
||||
return {
|
||||
"openai": {
|
||||
"configured": bool(api_keys.get('openai_api_key')),
|
||||
"value": api_keys.get('openai_api_key')[:8] + "..." if api_keys.get('openai_api_key') else None
|
||||
"configured": bool(api_keys_data.get('openai_api_key')),
|
||||
"value": api_keys_data.get('openai_api_key')[:8] + "..." if api_keys_data.get('openai_api_key') else None
|
||||
},
|
||||
"anthropic": {
|
||||
"configured": bool(api_keys.get('anthropic_api_key')),
|
||||
"value": api_keys.get('anthropic_api_key')[:8] + "..." if api_keys.get('anthropic_api_key') else None
|
||||
"configured": bool(api_keys_data.get('anthropic_api_key')),
|
||||
"value": api_keys_data.get('anthropic_api_key')[:8] + "..." if api_keys_data.get('anthropic_api_key') else None
|
||||
},
|
||||
"google": {
|
||||
"configured": bool(api_keys.get('google_api_key')),
|
||||
"value": api_keys.get('google_api_key')[:8] + "..." if api_keys.get('google_api_key') else None
|
||||
"configured": bool(api_keys_data.get('google_api_key')),
|
||||
"value": api_keys_data.get('google_api_key')[:8] + "..." if api_keys_data.get('google_api_key') else None
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
@@ -101,40 +104,6 @@ class OnboardingSummaryService:
|
||||
"google": {"configured": False, "value": None}
|
||||
}
|
||||
|
||||
def _get_website_analysis(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get website analysis data from database."""
|
||||
try:
|
||||
db = next(get_db())
|
||||
website_data = self.db_service.get_website_analysis(self.user_id, db)
|
||||
db.close()
|
||||
return website_data
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting website analysis: {str(e)}")
|
||||
return None
|
||||
|
||||
async def get_website_analysis_data(self) -> Dict[str, Any]:
|
||||
"""Get website analysis data for API endpoint."""
|
||||
try:
|
||||
website_analysis = self._get_website_analysis()
|
||||
return {
|
||||
"website_analysis": website_analysis,
|
||||
"status": "success" if website_analysis else "no_data"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_website_analysis_data: {str(e)}")
|
||||
raise e
|
||||
|
||||
def _get_research_preferences(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get research preferences from database."""
|
||||
try:
|
||||
db = next(get_db())
|
||||
preferences = self.db_service.get_research_preferences(self.user_id, db)
|
||||
db.close()
|
||||
return preferences
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting research preferences: {str(e)}")
|
||||
return None
|
||||
|
||||
def _get_personalization_settings(self, research_preferences: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""Get personalization settings based on research preferences."""
|
||||
if not research_preferences:
|
||||
@@ -194,4 +163,4 @@ class OnboardingSummaryService:
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting research preferences data: {e}")
|
||||
raise
|
||||
raise
|
||||
|
||||
@@ -13,7 +13,7 @@ class PersonaManagementService:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def check_persona_generation_readiness(self, user_id: int = 1) -> Dict[str, Any]:
|
||||
async def check_persona_generation_readiness(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Check if user has sufficient data for persona generation."""
|
||||
try:
|
||||
from api.persona import validate_persona_generation_readiness
|
||||
@@ -22,7 +22,7 @@ class PersonaManagementService:
|
||||
logger.error(f"Error checking persona readiness: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def generate_persona_preview(self, user_id: int = 1) -> Dict[str, Any]:
|
||||
async def generate_persona_preview(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Generate a preview of the writing persona without saving."""
|
||||
try:
|
||||
from api.persona import generate_persona_preview
|
||||
@@ -31,7 +31,7 @@ class PersonaManagementService:
|
||||
logger.error(f"Error generating persona preview: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def generate_writing_persona(self, user_id: int = 1) -> Dict[str, Any]:
|
||||
async def generate_writing_persona(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Generate and save a writing persona from onboarding data."""
|
||||
try:
|
||||
from api.persona import generate_persona, PersonaGenerationRequest
|
||||
@@ -41,7 +41,7 @@ class PersonaManagementService:
|
||||
logger.error(f"Error generating writing persona: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def get_user_writing_personas(self, user_id: int = 1) -> Dict[str, Any]:
|
||||
async def get_user_writing_personas(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Get all writing personas for the user."""
|
||||
try:
|
||||
from api.persona import get_user_personas
|
||||
|
||||
@@ -62,7 +62,7 @@ class Step3ResearchService:
|
||||
logger.info(f"Starting research analysis for user {user_id}, URL: {user_url}")
|
||||
|
||||
# Find the correct onboarding session for this user
|
||||
with get_db_session() as db:
|
||||
with get_db_session(user_id) as db:
|
||||
from models.onboarding import OnboardingSession
|
||||
session = db.query(OnboardingSession).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
@@ -108,17 +108,18 @@ class Step3ResearchService:
|
||||
industry_context
|
||||
)
|
||||
|
||||
# Store research data in database
|
||||
await self._store_research_data(
|
||||
session_id=actual_session_id,
|
||||
user_url=user_url,
|
||||
competitors=enhanced_competitors,
|
||||
industry_context=industry_context,
|
||||
analysis_metadata={
|
||||
**competitor_results,
|
||||
"social_media_data": social_media_results
|
||||
}
|
||||
)
|
||||
# Store research data in database - DEPRECATED in favor of delayed persistence in StepManagementService
|
||||
# await self._store_research_data(
|
||||
# session_id=actual_session_id,
|
||||
# user_id=user_id,
|
||||
# user_url=user_url,
|
||||
# competitors=enhanced_competitors,
|
||||
# industry_context=industry_context,
|
||||
# analysis_metadata={
|
||||
# **competitor_results,
|
||||
# "social_media_data": social_media_results
|
||||
# }
|
||||
# )
|
||||
|
||||
# Generate research summary
|
||||
research_summary = self._generate_research_summary(
|
||||
@@ -393,145 +394,21 @@ class Step3ResearchService:
|
||||
"competitive_landscape": "moderate" if high_threat_count < len(competitors) * 0.5 else "high"
|
||||
}
|
||||
|
||||
async def _store_research_data(
|
||||
self,
|
||||
session_id: str,
|
||||
user_url: str,
|
||||
competitors: List[Dict[str, Any]],
|
||||
industry_context: Optional[str],
|
||||
analysis_metadata: Dict[str, Any]
|
||||
) -> bool:
|
||||
"""
|
||||
Store research data in the database.
|
||||
|
||||
Args:
|
||||
session_id: Onboarding session ID
|
||||
user_url: User's website URL
|
||||
competitors: Competitor data
|
||||
industry_context: Industry context
|
||||
analysis_metadata: Analysis metadata
|
||||
|
||||
Returns:
|
||||
Boolean indicating success
|
||||
"""
|
||||
try:
|
||||
with get_db_session() as db:
|
||||
# Get onboarding session
|
||||
session = db.query(OnboardingSession).filter(
|
||||
OnboardingSession.id == int(session_id)
|
||||
).first()
|
||||
|
||||
if not session:
|
||||
logger.error(f"Onboarding session {session_id} not found")
|
||||
return False
|
||||
|
||||
# Store each competitor in CompetitorAnalysis table
|
||||
from models.onboarding import CompetitorAnalysis
|
||||
|
||||
logger.warning(f"🔍 COMPETITOR SAVE: Starting to save {len(competitors)} competitors for session {session_id}")
|
||||
logger.warning(f" Session ID: {session.id}")
|
||||
logger.warning(f" Session user_id: {session.user_id}")
|
||||
|
||||
saved_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for idx, competitor in enumerate(competitors):
|
||||
try:
|
||||
logger.warning(f"🔍 COMPETITOR SAVE: Saving competitor {idx + 1}/{len(competitors)}")
|
||||
logger.warning(f" Competitor URL: {competitor.get('url', 'N/A')}")
|
||||
logger.warning(f" Competitor Domain: {competitor.get('domain', 'N/A')}")
|
||||
logger.warning(f" Has title: {bool(competitor.get('title'))}")
|
||||
logger.warning(f" Has summary: {bool(competitor.get('summary'))}")
|
||||
logger.warning(f" Has competitive_insights: {bool(competitor.get('competitive_insights'))}")
|
||||
logger.warning(f" Has content_insights: {bool(competitor.get('content_insights'))}")
|
||||
|
||||
# Create competitor analysis record
|
||||
analysis_data = {
|
||||
"title": competitor.get("title", ""),
|
||||
"summary": competitor.get("summary", ""),
|
||||
"relevance_score": competitor.get("relevance_score", 0.5),
|
||||
"highlights": competitor.get("highlights", []),
|
||||
"favicon": competitor.get("favicon"),
|
||||
"image": competitor.get("image"),
|
||||
"published_date": competitor.get("published_date"),
|
||||
"author": competitor.get("author"),
|
||||
"competitive_analysis": competitor.get("competitive_insights", {}),
|
||||
"content_insights": competitor.get("content_insights", {}),
|
||||
"industry_context": industry_context,
|
||||
"analysis_metadata": analysis_metadata,
|
||||
"completed_at": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
logger.warning(f" analysis_data keys: {list(analysis_data.keys())}")
|
||||
logger.warning(f" competitive_analysis type: {type(analysis_data.get('competitive_analysis'))}")
|
||||
logger.warning(f" content_insights type: {type(analysis_data.get('content_insights'))}")
|
||||
|
||||
competitor_record = CompetitorAnalysis(
|
||||
session_id=session.id,
|
||||
competitor_url=competitor.get("url", ""),
|
||||
competitor_domain=competitor.get("domain", ""),
|
||||
analysis_data=analysis_data,
|
||||
status="completed"
|
||||
)
|
||||
|
||||
db.add(competitor_record)
|
||||
saved_count += 1
|
||||
logger.warning(f" ✅ Added competitor record {idx + 1} to session")
|
||||
|
||||
except Exception as e:
|
||||
failed_count += 1
|
||||
logger.error(f" ❌ Failed to save competitor {idx + 1}: {str(e)}")
|
||||
logger.error(f" Traceback: {traceback.format_exc()}")
|
||||
|
||||
# Store summary in session for quick access (backward compatibility)
|
||||
research_summary = {
|
||||
"user_url": user_url,
|
||||
"total_competitors": len(competitors),
|
||||
"industry_context": industry_context,
|
||||
"completed_at": datetime.utcnow().isoformat(),
|
||||
"analysis_metadata": analysis_metadata
|
||||
}
|
||||
|
||||
# Store summary in session (this requires step_data field to exist)
|
||||
# For now, we'll skip this since the model doesn't have step_data
|
||||
# TODO: Add step_data JSON column to OnboardingSession model if needed
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
logger.warning(f"🔍 COMPETITOR SAVE: ✅ Committed {saved_count} competitors to database")
|
||||
logger.warning(f" Failed: {failed_count}")
|
||||
|
||||
# Verify the save by querying back
|
||||
from models.onboarding import CompetitorAnalysis
|
||||
verify_count = db.query(CompetitorAnalysis).filter(
|
||||
CompetitorAnalysis.session_id == session.id
|
||||
).count()
|
||||
logger.warning(f"🔍 COMPETITOR SAVE: Verification - {verify_count} competitors found in DB for session {session.id}")
|
||||
|
||||
logger.info(f"Stored {len(competitors)} competitors in CompetitorAnalysis table for session {session_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"❌ COMPETITOR SAVE: Failed to commit competitors: {str(e)}")
|
||||
logger.error(f" Traceback: {traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error storing research data: {str(e)}", exc_info=True)
|
||||
return False
|
||||
# _store_research_data removed as it is now handled by StepManagementService via delayed persistence
|
||||
|
||||
async def get_research_data(self, session_id: str) -> Dict[str, Any]:
|
||||
async def get_research_data(self, session_id: str, user_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Retrieve research data for a session.
|
||||
|
||||
Args:
|
||||
session_id: Onboarding session ID
|
||||
user_id: Clerk user ID for database access
|
||||
|
||||
Returns:
|
||||
Dictionary containing research data
|
||||
"""
|
||||
try:
|
||||
with get_db_session() as db:
|
||||
with get_db_session(user_id) as db:
|
||||
session = db.query(OnboardingSession).filter(
|
||||
OnboardingSession.id == session_id
|
||||
).first()
|
||||
@@ -571,7 +448,7 @@ class Step3ResearchService:
|
||||
"image": analysis_data.get("image"),
|
||||
"published_date": analysis_data.get("published_date"),
|
||||
"author": analysis_data.get("author"),
|
||||
"competitive_insights": analysis_data.get("competitive_analysis", {}),
|
||||
"competitive_analysis": analysis_data.get("competitive_analysis", {}),
|
||||
"content_insights": analysis_data.get("content_insights", {})
|
||||
}
|
||||
competitors.append(competitor_info)
|
||||
@@ -588,8 +465,12 @@ class Step3ResearchService:
|
||||
}
|
||||
mapped_competitors.append(mapped_comp)
|
||||
|
||||
# Regenerate research summary from the mapped competitors
|
||||
research_summary = self._generate_research_summary(mapped_competitors, None)
|
||||
|
||||
research_data = {
|
||||
"competitors": mapped_competitors,
|
||||
"research_summary": research_summary,
|
||||
"completed_at": competitor_records[0].created_at.isoformat() if competitor_records[0].created_at else None
|
||||
}
|
||||
except Exception as e:
|
||||
|
||||
@@ -9,7 +9,7 @@ Version: 1.0
|
||||
Last Updated: January 2025
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends
|
||||
from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends, Body
|
||||
from pydantic import BaseModel, HttpUrl, Field
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime
|
||||
@@ -19,6 +19,15 @@ from loguru import logger
|
||||
from middleware.auth_middleware import get_current_user
|
||||
from .step3_research_service import Step3ResearchService
|
||||
from services.seo_tools.sitemap_service import SitemapService
|
||||
from services.database import get_session_for_user
|
||||
from api.content_planning.services.content_strategy.onboarding import OnboardingDataIntegrationService
|
||||
from models.website_analysis_monitoring_models import (
|
||||
DeepCompetitorAnalysisTask,
|
||||
DeepCompetitorAnalysisExecutionLog,
|
||||
DeepWebsiteCrawlTask,
|
||||
DeepWebsiteCrawlExecutionLog
|
||||
)
|
||||
from services.research.deep_crawl_service import DeepCrawlService
|
||||
|
||||
router = APIRouter(prefix="/api/onboarding/step3", tags=["Onboarding Step 3 - Research"])
|
||||
|
||||
@@ -59,6 +68,104 @@ class ResearchDataResponse(BaseModel):
|
||||
research_data: Optional[Dict[str, Any]] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
@router.get("/scheduled-tasks-status")
|
||||
async def scheduled_tasks_status(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
user_id = str(current_user.get("id"))
|
||||
db = get_session_for_user(user_id)
|
||||
if not db:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed")
|
||||
|
||||
try:
|
||||
integration_service = OnboardingDataIntegrationService()
|
||||
integrated = integration_service.get_integrated_data_sync(user_id, db)
|
||||
|
||||
# Check for competitors in competitor_analysis (Step 3 persistence) first
|
||||
competitors = integrated.get("competitor_analysis") if isinstance(integrated, dict) else []
|
||||
|
||||
# If not found, fall back to research_preferences
|
||||
if not competitors:
|
||||
research_prefs = integrated.get("research_preferences", {}) if isinstance(integrated, dict) else {}
|
||||
competitors = research_prefs.get("competitors") if isinstance(research_prefs, dict) else None
|
||||
|
||||
has_competitors = isinstance(competitors, list) and len(competitors) > 0
|
||||
|
||||
website_analysis = integrated.get("website_analysis") if isinstance(integrated, dict) else {}
|
||||
seo_audit = website_analysis.get("seo_audit") if isinstance(website_analysis, dict) else {}
|
||||
sitemap_benchmark_report = seo_audit.get("competitive_sitemap_benchmarking") if isinstance(seo_audit, dict) else None
|
||||
|
||||
# Check if it's a real report or just status tracking
|
||||
# A full report has 'analysis_type' or 'competitors' or 'benchmark'
|
||||
is_full_report = False
|
||||
if isinstance(sitemap_benchmark_report, dict):
|
||||
if "benchmark" in sitemap_benchmark_report or "competitors" in sitemap_benchmark_report:
|
||||
is_full_report = True
|
||||
|
||||
sitemap_benchmark_available = is_full_report
|
||||
sitemap_benchmark_last_run = sitemap_benchmark_report.get("timestamp") if isinstance(sitemap_benchmark_report, dict) else None
|
||||
sitemap_benchmark_status = sitemap_benchmark_report.get("status") if isinstance(sitemap_benchmark_report, dict) else None
|
||||
sitemap_benchmark_error = sitemap_benchmark_report.get("error") if isinstance(sitemap_benchmark_report, dict) else None
|
||||
|
||||
# Check for stale processing status (older than 30 minutes)
|
||||
if sitemap_benchmark_status == "processing" and isinstance(sitemap_benchmark_report, dict):
|
||||
started_at_str = sitemap_benchmark_report.get("started_at")
|
||||
if started_at_str:
|
||||
try:
|
||||
started_at = datetime.fromisoformat(started_at_str)
|
||||
if (datetime.utcnow() - started_at).total_seconds() > 600:
|
||||
sitemap_benchmark_status = "failed"
|
||||
sitemap_benchmark_error = "Task timed out (stale). Please retry."
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Extract error count from the report if available
|
||||
sitemap_error_count = 0
|
||||
if isinstance(sitemap_benchmark_report, dict):
|
||||
competitors_data = sitemap_benchmark_report.get("competitors", {})
|
||||
if isinstance(competitors_data, dict):
|
||||
errors = competitors_data.get("errors", {})
|
||||
if isinstance(errors, dict):
|
||||
sitemap_error_count = len(errors)
|
||||
|
||||
task = db.query(DeepCompetitorAnalysisTask).filter(
|
||||
DeepCompetitorAnalysisTask.user_id == user_id
|
||||
).order_by(DeepCompetitorAnalysisTask.updated_at.desc()).first()
|
||||
|
||||
latest_log = None
|
||||
if task:
|
||||
latest_log = db.query(DeepCompetitorAnalysisExecutionLog).filter(
|
||||
DeepCompetitorAnalysisExecutionLog.task_id == task.id
|
||||
).order_by(DeepCompetitorAnalysisExecutionLog.execution_date.desc()).first()
|
||||
|
||||
return {
|
||||
"deep_competitor_analysis": {
|
||||
"bulb": "green" if has_competitors else "red",
|
||||
"eligible": has_competitors,
|
||||
"reason": None if has_competitors else "No competitors found in Step 3 'Discovered Competitors'.",
|
||||
"task": {
|
||||
"exists": bool(task),
|
||||
"status": task.status if task else None,
|
||||
"next_execution": task.next_execution.isoformat() if task and task.next_execution else None,
|
||||
"last_run": latest_log.execution_date.isoformat() if latest_log and latest_log.execution_date else None,
|
||||
"last_status": latest_log.status if latest_log else None
|
||||
}
|
||||
},
|
||||
"competitive_sitemap_benchmarking": {
|
||||
"bulb": "green" if has_competitors else "red",
|
||||
"eligible": has_competitors,
|
||||
"reason": None if has_competitors else "No competitors found in Step 3 'Discovered Competitors'.",
|
||||
"report": {
|
||||
"available": sitemap_benchmark_available,
|
||||
"last_run": sitemap_benchmark_last_run,
|
||||
"error_count": sitemap_error_count,
|
||||
"status": sitemap_benchmark_status,
|
||||
"error": sitemap_benchmark_error
|
||||
}
|
||||
}
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
class ResearchHealthResponse(BaseModel):
|
||||
"""Response model for research service health check."""
|
||||
success: bool
|
||||
@@ -87,10 +194,57 @@ class SitemapAnalysisResponse(BaseModel):
|
||||
discovery_method: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
class SocialMediaDiscoveryRequest(BaseModel):
|
||||
"""Request model for social media discovery."""
|
||||
user_url: str = Field(..., description="User's website URL")
|
||||
|
||||
class SocialMediaDiscoveryResponse(BaseModel):
|
||||
"""Response model for social media discovery."""
|
||||
success: bool
|
||||
message: str
|
||||
social_media_accounts: Optional[Dict[str, str]] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
# Initialize services
|
||||
step3_research_service = Step3ResearchService()
|
||||
sitemap_service = SitemapService()
|
||||
|
||||
@router.post("/discover-social-media", response_model=SocialMediaDiscoveryResponse)
|
||||
async def discover_social_media(
|
||||
request: SocialMediaDiscoveryRequest,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> SocialMediaDiscoveryResponse:
|
||||
"""
|
||||
Discover social media accounts for a given website.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Starting social media discovery for user: {current_user.get('user_id', 'unknown')}")
|
||||
logger.info(f"Social media discovery request: {request.user_url}")
|
||||
|
||||
# Use ExaService directly via Step3ResearchService instance
|
||||
result = await step3_research_service.exa_service.discover_social_media_accounts(request.user_url)
|
||||
|
||||
if result["success"]:
|
||||
return SocialMediaDiscoveryResponse(
|
||||
success=True,
|
||||
message="Social media accounts discovered successfully",
|
||||
social_media_accounts=result.get("social_media_accounts", {})
|
||||
)
|
||||
else:
|
||||
return SocialMediaDiscoveryResponse(
|
||||
success=False,
|
||||
message="Social media discovery failed",
|
||||
error=result.get("error", "Unknown error")
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in social media discovery: {str(e)}")
|
||||
return SocialMediaDiscoveryResponse(
|
||||
success=False,
|
||||
message="An unexpected error occurred",
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@router.post("/discover-competitors", response_model=CompetitorDiscoveryResponse)
|
||||
async def discover_competitors(
|
||||
request: CompetitorDiscoveryRequest,
|
||||
@@ -168,7 +322,10 @@ async def discover_competitors(
|
||||
)
|
||||
|
||||
@router.post("/research-data", response_model=ResearchDataResponse)
|
||||
async def get_research_data(request: ResearchDataRequest) -> ResearchDataResponse:
|
||||
async def get_research_data(
|
||||
request: ResearchDataRequest,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> ResearchDataResponse:
|
||||
"""
|
||||
Retrieve research data for a specific onboarding session.
|
||||
|
||||
@@ -176,7 +333,10 @@ async def get_research_data(request: ResearchDataRequest) -> ResearchDataRespons
|
||||
and research summary for the given session.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Retrieving research data for session {request.session_id}")
|
||||
# Get Clerk user ID for user isolation
|
||||
clerk_user_id = str(current_user.get('id'))
|
||||
|
||||
logger.info(f"Retrieving research data for session {request.session_id} (user: {clerk_user_id})")
|
||||
|
||||
# Validate session ID
|
||||
if not request.session_id or len(request.session_id) < 10:
|
||||
@@ -186,7 +346,7 @@ async def get_research_data(request: ResearchDataRequest) -> ResearchDataRespons
|
||||
)
|
||||
|
||||
# Retrieve research data
|
||||
result = await step3_research_service.get_research_data(request.session_id)
|
||||
result = await step3_research_service.get_research_data(request.session_id, clerk_user_id)
|
||||
|
||||
if result["success"]:
|
||||
logger.info(f"Successfully retrieved research data for session {request.session_id}")
|
||||
@@ -220,6 +380,32 @@ async def get_research_data(request: ResearchDataRequest) -> ResearchDataRespons
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@router.get("/sitemap-benchmark-report")
|
||||
async def get_sitemap_benchmark_report(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""
|
||||
Retrieve the full sitemap benchmark report for the current user.
|
||||
"""
|
||||
user_id = str(current_user.get("id"))
|
||||
db = get_session_for_user(user_id)
|
||||
if not db:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed")
|
||||
|
||||
try:
|
||||
integration_service = OnboardingDataIntegrationService()
|
||||
integrated = integration_service.get_integrated_data_sync(user_id, db)
|
||||
|
||||
website_analysis = integrated.get("website_analysis") if isinstance(integrated, dict) else {}
|
||||
seo_audit = website_analysis.get("seo_audit") if isinstance(website_analysis, dict) else {}
|
||||
sitemap_benchmark_report = seo_audit.get("competitive_sitemap_benchmarking") if isinstance(seo_audit, dict) else None
|
||||
|
||||
if not sitemap_benchmark_report:
|
||||
raise HTTPException(status_code=404, detail="No sitemap benchmark report found")
|
||||
|
||||
return sitemap_benchmark_report
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@router.get("/health", response_model=ResearchHealthResponse)
|
||||
async def health_check() -> ResearchHealthResponse:
|
||||
"""
|
||||
@@ -260,14 +446,17 @@ async def health_check() -> ResearchHealthResponse:
|
||||
)
|
||||
|
||||
@router.post("/validate-session")
|
||||
async def validate_session(session_id: str) -> Dict[str, Any]:
|
||||
async def validate_session(
|
||||
session_id: str = Body(..., embed=True),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate that a session exists and is ready for Step 3.
|
||||
|
||||
This endpoint checks if the session exists and has completed previous steps.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Validating session {session_id} for Step 3")
|
||||
logger.info(f"Validating session {session_id} for Step 3, user: {current_user.get('id')}")
|
||||
|
||||
# Basic validation
|
||||
if not session_id or len(session_id) < 10:
|
||||
@@ -290,12 +479,141 @@ async def validate_session(session_id: str) -> Dict[str, Any]:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating session: {str(e)}")
|
||||
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# Deep Website Crawl Endpoints
|
||||
|
||||
class DeepCrawlRequest(BaseModel):
|
||||
user_url: str
|
||||
schedule: bool = False
|
||||
|
||||
@router.post("/deep-crawl/start")
|
||||
async def start_deep_crawl(
|
||||
request: DeepCrawlRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Start a deep website crawl task.
|
||||
If schedule is True, it sets up the recurring task.
|
||||
If schedule is False, it runs immediately (fire and forget/poll).
|
||||
"""
|
||||
user_id = str(current_user.get("id"))
|
||||
db = get_session_for_user(user_id)
|
||||
if not db:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed")
|
||||
|
||||
try:
|
||||
# Check/Create Task
|
||||
task = db.query(DeepWebsiteCrawlTask).filter(
|
||||
DeepWebsiteCrawlTask.user_id == user_id,
|
||||
DeepWebsiteCrawlTask.website_url == request.user_url
|
||||
).first()
|
||||
|
||||
if not task:
|
||||
task = DeepWebsiteCrawlTask(
|
||||
user_id=user_id,
|
||||
website_url=request.user_url,
|
||||
status="active" if request.schedule else "running",
|
||||
next_execution=datetime.utcnow() if request.schedule else None
|
||||
)
|
||||
db.add(task)
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
else:
|
||||
task.website_url = request.user_url # Update URL if changed?
|
||||
if request.schedule:
|
||||
task.status = "active"
|
||||
# If scheduling, don't run immediately unless requested?
|
||||
# User said "fire ... OR let it be scheduled".
|
||||
# If this endpoint is called, we assume intent to start OR schedule.
|
||||
# If schedule=True, we might just set it active.
|
||||
# If schedule=False, we run it now.
|
||||
# But typically user might want "Run now AND schedule".
|
||||
# Let's assume this endpoint is "Start Now". Scheduling is separate?
|
||||
# "option to fire and check ... or let it be scheduled"
|
||||
# If "fire", run now.
|
||||
pass
|
||||
else:
|
||||
task.status = "running"
|
||||
db.commit()
|
||||
|
||||
if not request.schedule:
|
||||
# Run immediately in background
|
||||
service = DeepCrawlService()
|
||||
background_tasks.add_task(
|
||||
service.execute_deep_crawl,
|
||||
user_id=user_id,
|
||||
website_url=request.user_url,
|
||||
task_id=task.id
|
||||
)
|
||||
message = "Deep crawl started immediately."
|
||||
else:
|
||||
# Scheduled
|
||||
task.status = "active"
|
||||
task.next_execution = datetime.utcnow() # Scheduler will pick it up
|
||||
db.commit()
|
||||
message = "Deep crawl scheduled."
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Session validation failed",
|
||||
"error": str(e)
|
||||
"success": True,
|
||||
"message": message,
|
||||
"task_id": task.id,
|
||||
"status": task.status
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting deep crawl: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/deep-crawl/status")
|
||||
async def get_deep_crawl_status(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get status of the deep website crawl task.
|
||||
"""
|
||||
user_id = str(current_user.get("id"))
|
||||
db = get_session_for_user(user_id)
|
||||
if not db:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed")
|
||||
|
||||
try:
|
||||
task = db.query(DeepWebsiteCrawlTask).filter(
|
||||
DeepWebsiteCrawlTask.user_id == user_id
|
||||
).order_by(DeepWebsiteCrawlTask.id.desc()).first()
|
||||
|
||||
if not task:
|
||||
return {
|
||||
"exists": False,
|
||||
"status": None
|
||||
}
|
||||
|
||||
latest_log = db.query(DeepWebsiteCrawlExecutionLog).filter(
|
||||
DeepWebsiteCrawlExecutionLog.task_id == task.id
|
||||
).order_by(DeepWebsiteCrawlExecutionLog.execution_date.desc()).first()
|
||||
|
||||
return {
|
||||
"exists": True,
|
||||
"task_id": task.id,
|
||||
"status": task.status,
|
||||
"last_executed": task.last_executed,
|
||||
"next_execution": task.next_execution,
|
||||
"latest_log": {
|
||||
"status": latest_log.status if latest_log else None,
|
||||
"execution_date": latest_log.execution_date if latest_log else None,
|
||||
"result_summary": latest_log.result_data if latest_log else None,
|
||||
"error": latest_log.error_message if latest_log else None
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting deep crawl status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@router.get("/cost-estimate")
|
||||
async def get_cost_estimate(
|
||||
@@ -421,7 +739,8 @@ async def analyze_sitemap_for_onboarding(
|
||||
competitors=request.competitors,
|
||||
industry_context=request.industry_context,
|
||||
analyze_content_trends=request.analyze_content_trends,
|
||||
analyze_publishing_patterns=request.analyze_publishing_patterns
|
||||
analyze_publishing_patterns=request.analyze_publishing_patterns,
|
||||
user_id=str(current_user.get('id'))
|
||||
)
|
||||
|
||||
# Check if analysis was successful
|
||||
|
||||
196
backend/api/onboarding_utils/step4_asset_routes.py
Normal file
196
backend/api/onboarding_utils/step4_asset_routes.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""
|
||||
Step 4 Brand Asset Routes
|
||||
Handles brand avatar generation, enhancement, and variation.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
from fastapi import APIRouter, HTTPException, Depends, File, UploadFile, Form
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
from loguru import logger
|
||||
from .step4_persona_routes import _extract_user_id
|
||||
import base64
|
||||
import os
|
||||
from pathlib import Path
|
||||
from utils.file_storage import save_file_safely, generate_unique_filename
|
||||
from services.database import get_db, WORKSPACE_DIR
|
||||
from utils.asset_tracker import save_asset_to_library
|
||||
|
||||
from services.llm_providers.main_image_generation import (
|
||||
generate_image_with_provider,
|
||||
enhance_image_prompt,
|
||||
generate_image_variation
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# --- Models ---
|
||||
class AvatarPromptRequest(BaseModel):
|
||||
user_id: Optional[str] = None
|
||||
prompt: str
|
||||
aspect_ratio: str = "1:1"
|
||||
style_preset: Optional[str] = None
|
||||
negative_prompt: Optional[str] = None
|
||||
num_inference_steps: int = 30
|
||||
guidance_scale: float = 7.5
|
||||
|
||||
class AvatarEnhanceRequest(BaseModel):
|
||||
user_id: Optional[str] = None
|
||||
prompt: str
|
||||
|
||||
class VoiceCloneRequest(BaseModel):
|
||||
user_id: Optional[str] = None
|
||||
voice_name: str
|
||||
description: Optional[str] = None
|
||||
engine: str = "qwen3" # qwen3 or minimax
|
||||
|
||||
# --- Routes ---
|
||||
|
||||
@router.post("/generate-avatar")
|
||||
async def generate_avatar(
|
||||
request: AvatarPromptRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Generate a brand avatar using available image providers."""
|
||||
try:
|
||||
user_id = _extract_user_id(request.user_id)
|
||||
|
||||
logger.info(f"Generating avatar for user {user_id} with prompt: {request.prompt}")
|
||||
|
||||
# 1. Generate Image
|
||||
result = await generate_image_with_provider(
|
||||
prompt=request.prompt,
|
||||
aspect_ratio=request.aspect_ratio,
|
||||
negative_prompt=request.negative_prompt,
|
||||
num_inference_steps=request.num_inference_steps,
|
||||
guidance_scale=request.guidance_scale,
|
||||
style_preset=request.style_preset,
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
if not result.get("success"):
|
||||
raise HTTPException(status_code=500, detail=result.get("error", "Generation failed"))
|
||||
|
||||
# 2. Save to local storage and Asset Library
|
||||
# The result typically contains image_base64 or image_url
|
||||
# For simplicity, we assume image_base64 is returned or we download the URL
|
||||
|
||||
image_data = result.get("image_base64")
|
||||
if not image_data and result.get("image_url"):
|
||||
# TODO: Download image from URL if needed, or just store URL
|
||||
pass
|
||||
|
||||
if image_data:
|
||||
# Decode if needed (usually it's already base64 string)
|
||||
# Save file
|
||||
filename = generate_unique_filename("avatar", "png")
|
||||
file_path = save_file_safely(
|
||||
base64.b64decode(image_data) if isinstance(image_data, str) else image_data,
|
||||
user_id,
|
||||
"avatars",
|
||||
filename
|
||||
)
|
||||
|
||||
# Save to Asset Library
|
||||
asset_id = save_asset_to_library(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
file_path=file_path,
|
||||
asset_type="image",
|
||||
category="brand_avatar",
|
||||
meta_data={
|
||||
"prompt": request.prompt,
|
||||
"provider": result.get("provider", "unknown"),
|
||||
"style": request.style_preset
|
||||
}
|
||||
)
|
||||
|
||||
# Construct public URL (this depends on your static file serving setup)
|
||||
# Assuming /api/assets/{user_id}/avatars/{filename}
|
||||
image_url = f"/api/assets/{user_id}/avatars/{filename}"
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"image_url": image_url,
|
||||
"image_base64": image_data, # Optional: return base64 for immediate display
|
||||
"asset_id": asset_id
|
||||
}
|
||||
|
||||
return {"success": False, "error": "No image data returned"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Avatar generation failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/enhance-prompt")
|
||||
async def enhance_prompt_route(
|
||||
request: AvatarEnhanceRequest
|
||||
):
|
||||
"""Enhance a simple prompt into a detailed midjourney-style prompt."""
|
||||
try:
|
||||
user_id = _extract_user_id(request.user_id)
|
||||
logger.info(f"Enhancing prompt for user {user_id}: {request.prompt}")
|
||||
|
||||
enhanced_prompt = await enhance_image_prompt(request.prompt)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"original_prompt": request.prompt,
|
||||
"optimized_prompt": enhanced_prompt
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Prompt enhancement failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/create-voice-clone")
|
||||
async def create_voice_clone(
|
||||
voice_name: str = Form(...),
|
||||
description: str = Form(None),
|
||||
engine: str = Form("qwen3"),
|
||||
file: UploadFile = File(...),
|
||||
user_id: Optional[str] = Form(None),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a voice clone from an audio file."""
|
||||
try:
|
||||
user_id = _extract_user_id(user_id)
|
||||
logger.info(f"Creating voice clone '{voice_name}' for user {user_id}")
|
||||
|
||||
# 1. Save uploaded audio file
|
||||
file_content = await file.read()
|
||||
filename = generate_unique_filename("voice_sample", Path(file.filename).suffix.lstrip("."))
|
||||
file_path = save_file_safely(file_content, user_id, "voice_samples", filename)
|
||||
|
||||
# 2. Call Voice Cloning API (Placeholder for actual implementation)
|
||||
# TODO: Integrate with Minimax or CosyVoice API
|
||||
# For now, we simulate success
|
||||
|
||||
# 3. Save to Asset Library
|
||||
asset_id = save_asset_to_library(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
file_path=file_path,
|
||||
asset_type="audio",
|
||||
category="voice_clone",
|
||||
meta_data={
|
||||
"voice_name": voice_name,
|
||||
"engine": engine,
|
||||
"description": description,
|
||||
"original_filename": file.filename
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"custom_voice_id": f"vc_{asset_id}", # Mock ID
|
||||
"preview_audio_url": f"/api/assets/{user_id}/voice_samples/{filename}",
|
||||
"asset_id": asset_id,
|
||||
"message": "Voice clone created successfully (simulated)"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Voice cloning failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -202,11 +202,24 @@ async def get_latest_persona(current_user: Dict[str, Any] = Depends(get_current_
|
||||
raise HTTPException(status_code=404, detail="Cached persona expired")
|
||||
|
||||
return {"success": True, "persona": cached}
|
||||
except HTTPException:
|
||||
raise
|
||||
except HTTPException as he:
|
||||
# Return 200 even for HTTP exceptions (like 404) to prevent frontend connection errors
|
||||
# if the endpoint is called during an auto-initialization phase.
|
||||
logger.warning(f"Persona retrieval notice (returning success=False): {he.detail}")
|
||||
return {
|
||||
"success": False,
|
||||
"persona": None,
|
||||
"message": he.detail,
|
||||
"status_code": he.status_code
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting latest persona: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
logger.error(f"Error getting latest persona: {e}", exc_info=True)
|
||||
return {
|
||||
"success": False,
|
||||
"persona": None,
|
||||
"message": f"Internal error retrieving persona: {str(e)}",
|
||||
"status_code": 500
|
||||
}
|
||||
|
||||
@router.post("/step4/persona-save", response_model=Dict[str, Any])
|
||||
async def save_persona_update(
|
||||
@@ -228,8 +241,12 @@ async def save_persona_update(
|
||||
logger.info(f"Saved latest persona to cache for user {user_id}")
|
||||
return {"success": True}
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving latest persona: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
logger.error(f"Error saving latest persona: {e}", exc_info=True)
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Failed to save persona: {str(e)}",
|
||||
"status_code": 500
|
||||
}
|
||||
|
||||
@router.get("/step4/persona-task/{task_id}", response_model=PersonaTaskStatus)
|
||||
async def get_persona_task_status(task_id: str):
|
||||
|
||||
@@ -4,24 +4,315 @@ Handles onboarding step operations and progress tracking.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from services.onboarding.progress_service import get_onboarding_progress_service
|
||||
from services.onboarding.database_service import OnboardingDatabaseService
|
||||
from api.content_planning.services.content_strategy.onboarding import OnboardingDataIntegrationService
|
||||
from services.database import get_db
|
||||
from models.onboarding import OnboardingSession, APIKey, WebsiteAnalysis, ResearchPreferences, PersonaData, CompetitorAnalysis
|
||||
|
||||
class StepManagementService:
|
||||
"""Service for handling onboarding step management."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
self.integration_service = OnboardingDataIntegrationService()
|
||||
|
||||
def _get_or_create_session(self, user_id: str, db: Session) -> OnboardingSession:
|
||||
"""Get or create onboarding session."""
|
||||
session = db.query(OnboardingSession).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not session:
|
||||
session = OnboardingSession(
|
||||
user_id=user_id,
|
||||
current_step=1,
|
||||
progress=0.0,
|
||||
started_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
db.add(session)
|
||||
db.commit()
|
||||
db.refresh(session)
|
||||
|
||||
return session
|
||||
|
||||
def _save_api_key(self, user_id: str, provider: str, api_key: str, db: Session) -> bool:
|
||||
"""Save API key directly to database."""
|
||||
try:
|
||||
session = self._get_or_create_session(user_id, db)
|
||||
|
||||
existing_key = db.query(APIKey).filter(
|
||||
APIKey.session_id == session.id,
|
||||
APIKey.provider == provider
|
||||
).first()
|
||||
|
||||
if existing_key:
|
||||
existing_key.key = api_key
|
||||
existing_key.updated_at = datetime.utcnow()
|
||||
else:
|
||||
new_key = APIKey(
|
||||
session_id=session.id,
|
||||
provider=provider,
|
||||
key=api_key
|
||||
)
|
||||
db.add(new_key)
|
||||
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving API key for user {user_id}: {e}")
|
||||
db.rollback()
|
||||
raise e
|
||||
|
||||
def _save_website_analysis(self, user_id: str, analysis_data: Dict[str, Any], db: Session) -> bool:
|
||||
"""Save website analysis directly to database."""
|
||||
try:
|
||||
session = self._get_or_create_session(user_id, db)
|
||||
|
||||
# Normalize payload
|
||||
incoming = analysis_data or {}
|
||||
nested = incoming.get('analysis') if isinstance(incoming.get('analysis'), dict) else None
|
||||
|
||||
# Extract extra fields
|
||||
brand_analysis = (nested or incoming).get('brand_analysis')
|
||||
content_strategy_insights = (nested or incoming).get('content_strategy_insights')
|
||||
meta_info = (nested or incoming).get('meta_info')
|
||||
|
||||
# Fix: Check both nested and incoming for social_media_presence
|
||||
social_media_presence = (nested or {}).get('social_media_presence') or incoming.get('social_media_presence')
|
||||
|
||||
seo_audit = (nested or incoming).get('seo_audit')
|
||||
style_patterns = (nested or incoming).get('style_patterns')
|
||||
style_guidelines = (nested or incoming).get('guidelines')
|
||||
sitemap_analysis = (nested or incoming).get('sitemap_analysis')
|
||||
|
||||
# Prepare crawl_result
|
||||
crawl_result = incoming.get('crawl_result') or {}
|
||||
if not isinstance(crawl_result, dict):
|
||||
crawl_result = {"raw": crawl_result}
|
||||
|
||||
# Meta info still goes to crawl_result as we didn't add a column for it
|
||||
if meta_info:
|
||||
crawl_result['meta_info'] = meta_info
|
||||
|
||||
# Store sitemap_analysis in crawl_result as we don't have a dedicated column yet
|
||||
if sitemap_analysis:
|
||||
crawl_result['sitemap_analysis'] = sitemap_analysis
|
||||
|
||||
normalized = {
|
||||
'website_url': incoming.get('website') or incoming.get('website_url') or '',
|
||||
'writing_style': (nested or incoming).get('writing_style'),
|
||||
'content_characteristics': (nested or incoming).get('content_characteristics'),
|
||||
'target_audience': (nested or incoming).get('target_audience'),
|
||||
'content_type': (nested or incoming).get('content_type'),
|
||||
'recommended_settings': (nested or incoming).get('recommended_settings'),
|
||||
'brand_analysis': brand_analysis,
|
||||
'content_strategy_insights': content_strategy_insights,
|
||||
'social_media_presence': social_media_presence,
|
||||
'crawl_result': crawl_result,
|
||||
'seo_audit': seo_audit,
|
||||
'style_patterns': style_patterns,
|
||||
'style_guidelines': style_guidelines
|
||||
}
|
||||
|
||||
# Filter only valid columns to prevent TypeError
|
||||
valid_columns = [c.name for c in WebsiteAnalysis.__table__.columns if c.name not in ['id', 'session_id', 'created_at', 'updated_at']]
|
||||
filtered_data = {k: v for k, v in normalized.items() if k in valid_columns and v is not None}
|
||||
|
||||
existing_analysis = db.query(WebsiteAnalysis).filter(
|
||||
WebsiteAnalysis.session_id == session.id
|
||||
).first()
|
||||
|
||||
if existing_analysis:
|
||||
for key, value in filtered_data.items():
|
||||
setattr(existing_analysis, key, value)
|
||||
existing_analysis.updated_at = datetime.utcnow()
|
||||
else:
|
||||
new_analysis = WebsiteAnalysis(
|
||||
session_id=session.id,
|
||||
**filtered_data
|
||||
)
|
||||
db.add(new_analysis)
|
||||
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving website analysis for user {user_id}: {e}")
|
||||
db.rollback()
|
||||
raise e
|
||||
|
||||
def _save_research_preferences(self, user_id: str, research_data: Dict[str, Any], db: Session) -> bool:
|
||||
"""Save research preferences directly to database."""
|
||||
try:
|
||||
session = self._get_or_create_session(user_id, db)
|
||||
|
||||
# Add defaults for required fields if missing to prevent 500 errors
|
||||
# The frontend Step 3 (Competitor Analysis) might not send these
|
||||
if 'research_depth' not in research_data:
|
||||
research_data['research_depth'] = 'Comprehensive'
|
||||
if 'content_types' not in research_data:
|
||||
research_data['content_types'] = ["Blog Posts", "Social Media", "Newsletters"]
|
||||
if 'auto_research' not in research_data:
|
||||
research_data['auto_research'] = True
|
||||
if 'factual_content' not in research_data:
|
||||
research_data['factual_content'] = True
|
||||
|
||||
existing_prefs = db.query(ResearchPreferences).filter(
|
||||
ResearchPreferences.session_id == session.id
|
||||
).first()
|
||||
|
||||
if existing_prefs:
|
||||
# Fix for SQLite DateTime issue: Ensure created_at is a datetime object
|
||||
if hasattr(existing_prefs, 'created_at') and isinstance(existing_prefs.created_at, str):
|
||||
try:
|
||||
existing_prefs.created_at = datetime.fromisoformat(existing_prefs.created_at)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
for key, value in research_data.items():
|
||||
# Skip metadata fields and id
|
||||
if key in ['id', 'session_id', 'created_at', 'updated_at']:
|
||||
continue
|
||||
|
||||
if hasattr(existing_prefs, key) and value is not None:
|
||||
setattr(existing_prefs, key, value)
|
||||
existing_prefs.updated_at = datetime.utcnow()
|
||||
else:
|
||||
# Filter valid columns only to avoid errors
|
||||
valid_columns = [c.name for c in ResearchPreferences.__table__.columns if c.name not in ['id', 'session_id', 'created_at', 'updated_at']]
|
||||
filtered_data = {k: v for k, v in research_data.items() if k in valid_columns}
|
||||
|
||||
new_prefs = ResearchPreferences(
|
||||
session_id=session.id,
|
||||
**filtered_data
|
||||
)
|
||||
db.add(new_prefs)
|
||||
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving research preferences for user {user_id}: {e}")
|
||||
db.rollback()
|
||||
raise e
|
||||
|
||||
def _save_competitor_analysis(self, user_id: str, competitors: List[Dict[str, Any]], industry_context: Optional[str], db: Session) -> bool:
|
||||
"""Save competitor analysis results to database."""
|
||||
try:
|
||||
session = self._get_or_create_session(user_id, db)
|
||||
|
||||
logger.info(f"🔍 COMPETITOR SAVE: Starting to save {len(competitors)} competitors for session {session.id}")
|
||||
|
||||
saved_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for idx, competitor in enumerate(competitors):
|
||||
try:
|
||||
if not competitor or not isinstance(competitor, dict):
|
||||
logger.warning(f" ⚠️ Skipping invalid competitor entry at index {idx}: {competitor}")
|
||||
continue
|
||||
|
||||
# Use full URL (Text column supports it) and clean it
|
||||
raw_url = competitor.get("url", "")
|
||||
competitor_url = raw_url.strip().strip('`').strip() if raw_url else ""
|
||||
|
||||
# Prepare analysis data
|
||||
analysis_data = {
|
||||
"title": competitor.get("title", ""),
|
||||
"summary": competitor.get("summary", ""),
|
||||
"relevance_score": competitor.get("relevance_score", 0.5),
|
||||
"highlights": competitor.get("highlights", []),
|
||||
"subpages": competitor.get("subpages", []),
|
||||
"favicon": competitor.get("favicon"),
|
||||
"image": competitor.get("image"),
|
||||
"published_date": competitor.get("published_date"),
|
||||
"author": competitor.get("author"),
|
||||
"competitive_analysis": competitor.get("competitive_analysis") or competitor.get("competitive_insights", {}),
|
||||
"content_insights": competitor.get("content_insights", {}),
|
||||
"industry_context": industry_context,
|
||||
"completed_at": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
# Check if competitor already exists for this session
|
||||
existing_competitor = db.query(CompetitorAnalysis).filter(
|
||||
CompetitorAnalysis.session_id == session.id,
|
||||
CompetitorAnalysis.competitor_url == competitor.get("url", "")
|
||||
).first()
|
||||
|
||||
has_details = bool(analysis_data.get("summary") or analysis_data.get("highlights"))
|
||||
detail_msg = "with rich details" if has_details else "basic info only"
|
||||
|
||||
if existing_competitor:
|
||||
existing_competitor.analysis_data = analysis_data
|
||||
existing_competitor.updated_at = datetime.utcnow()
|
||||
logger.info(f" Updated existing competitor {idx + 1} ({detail_msg})")
|
||||
else:
|
||||
competitor_record = CompetitorAnalysis(
|
||||
session_id=session.id,
|
||||
competitor_url=competitor_url,
|
||||
competitor_domain=competitor.get("domain", ""),
|
||||
analysis_data=analysis_data,
|
||||
status="completed"
|
||||
)
|
||||
db.add(competitor_record)
|
||||
logger.info(f" Added new competitor {idx + 1} ({detail_msg})")
|
||||
|
||||
saved_count += 1
|
||||
|
||||
except Exception as e:
|
||||
failed_count += 1
|
||||
logger.error(f" ❌ Failed to save competitor {idx + 1}: {str(e)}")
|
||||
|
||||
db.commit()
|
||||
logger.info(f"✅ Saved {saved_count} competitors ({failed_count} failed)")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving competitor analysis for user {user_id}: {e}")
|
||||
db.rollback()
|
||||
raise e
|
||||
|
||||
|
||||
def _save_persona_data(self, user_id: str, persona_data: Dict[str, Any], db: Session) -> bool:
|
||||
"""Save persona data directly to database."""
|
||||
try:
|
||||
session = self._get_or_create_session(user_id, db)
|
||||
|
||||
existing = db.query(PersonaData).filter(
|
||||
PersonaData.session_id == session.id
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
existing.core_persona = persona_data.get('corePersona')
|
||||
existing.platform_personas = persona_data.get('platformPersonas')
|
||||
existing.quality_metrics = persona_data.get('qualityMetrics')
|
||||
existing.selected_platforms = persona_data.get('selectedPlatforms', [])
|
||||
existing.updated_at = datetime.utcnow()
|
||||
else:
|
||||
persona = PersonaData(
|
||||
session_id=session.id,
|
||||
core_persona=persona_data.get('corePersona'),
|
||||
platform_personas=persona_data.get('platformPersonas'),
|
||||
quality_metrics=persona_data.get('qualityMetrics'),
|
||||
selected_platforms=persona_data.get('selectedPlatforms', [])
|
||||
)
|
||||
db.add(persona)
|
||||
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving persona data for user {user_id}: {e}")
|
||||
db.rollback()
|
||||
raise e
|
||||
|
||||
async def get_onboarding_status(self, current_user: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get the current onboarding status (per user)."""
|
||||
try:
|
||||
from services.onboarding.progress_service import OnboardingProgressService
|
||||
user_id = str(current_user.get('id'))
|
||||
status = get_onboarding_progress_service().get_onboarding_status(user_id)
|
||||
status = OnboardingProgressService().get_onboarding_status(user_id)
|
||||
return {
|
||||
"is_completed": status["is_completed"],
|
||||
"current_step": status["current_step"],
|
||||
@@ -38,8 +329,9 @@ class StepManagementService:
|
||||
async def get_onboarding_progress_full(self, current_user: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get the full onboarding progress data."""
|
||||
try:
|
||||
from services.onboarding.progress_service import OnboardingProgressService
|
||||
user_id = str(current_user.get('id'))
|
||||
progress_service = get_onboarding_progress_service()
|
||||
progress_service = OnboardingProgressService()
|
||||
status = progress_service.get_onboarding_status(user_id)
|
||||
data = progress_service.get_completion_data(user_id)
|
||||
|
||||
@@ -125,11 +417,13 @@ class StepManagementService:
|
||||
"""Get data for a specific step."""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
db = next(get_db())
|
||||
db_service = OnboardingDatabaseService()
|
||||
db = next(get_db(current_user))
|
||||
|
||||
# Use SSOT for reading step data
|
||||
integrated_data = self.integration_service.get_integrated_data_sync(user_id, db)
|
||||
|
||||
if step_number == 2:
|
||||
website = db_service.get_website_analysis(user_id, db) or {}
|
||||
website = integrated_data.get('website_analysis', {})
|
||||
return {
|
||||
"step_number": 2,
|
||||
"title": "Website",
|
||||
@@ -140,18 +434,27 @@ class StepManagementService:
|
||||
"validation_errors": []
|
||||
}
|
||||
if step_number == 3:
|
||||
research = db_service.get_research_preferences(user_id, db) or {}
|
||||
research = integrated_data.get('research_preferences', {})
|
||||
competitors = integrated_data.get('competitor_analysis', [])
|
||||
website = integrated_data.get('website_analysis', {})
|
||||
social_media = website.get('social_media_presence') or website.get('social_media_accounts', {})
|
||||
|
||||
# Merge competitors into the data
|
||||
step_data = research.copy() if research else {}
|
||||
step_data['competitors'] = competitors
|
||||
step_data['social_media_accounts'] = social_media
|
||||
|
||||
return {
|
||||
"step_number": 3,
|
||||
"title": "Research",
|
||||
"description": "Discover competitors",
|
||||
"status": 'completed' if (research.get('research_depth') or research.get('content_types')) else 'pending',
|
||||
"status": 'completed' if (research.get('research_depth') or research.get('content_types') or competitors) else 'pending',
|
||||
"completed_at": None,
|
||||
"data": research,
|
||||
"data": step_data,
|
||||
"validation_errors": []
|
||||
}
|
||||
if step_number == 4:
|
||||
persona = db_service.get_persona_data(user_id, db) or {}
|
||||
persona = integrated_data.get('persona_data', {})
|
||||
return {
|
||||
"step_number": 4,
|
||||
"title": "Personalization",
|
||||
@@ -162,7 +465,8 @@ class StepManagementService:
|
||||
"validation_errors": []
|
||||
}
|
||||
|
||||
status = get_onboarding_progress_service().get_onboarding_status(user_id)
|
||||
from services.onboarding.progress_service import OnboardingProgressService
|
||||
status = OnboardingProgressService().get_onboarding_status(user_id)
|
||||
mapping = {
|
||||
1: ('API Keys', 'Connect your AI services', status['current_step'] >= 1),
|
||||
5: ('Integrations', 'Connect additional services', status['current_step'] >= 5),
|
||||
@@ -201,8 +505,7 @@ class StepManagementService:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
db = next(get_db())
|
||||
db_service = OnboardingDatabaseService()
|
||||
db = next(get_db(current_user))
|
||||
|
||||
save_errors = [] # Track save failures
|
||||
|
||||
@@ -218,12 +521,9 @@ class StepManagementService:
|
||||
for provider, key in api_keys.items():
|
||||
if key:
|
||||
try:
|
||||
saved = db_service.save_api_key(user_id, provider, key, db)
|
||||
saved = self._save_api_key(user_id, provider, key, db)
|
||||
if saved:
|
||||
logger.info(f"✅ Saved API key for provider {provider}")
|
||||
else:
|
||||
# This should not happen anymore since save_api_key now raises exceptions
|
||||
raise Exception(f"API key save returned False for provider {provider}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ BLOCKING ERROR: Failed to save API key for provider {provider}: {str(e)}")
|
||||
raise HTTPException(
|
||||
@@ -236,18 +536,36 @@ class StepManagementService:
|
||||
website_data = request_data.get('data') or request_data
|
||||
logger.info(f"🔍 Step 2: Raw request_data keys: {list(request_data.keys()) if request_data else 'None'}")
|
||||
logger.info(f"🔍 Step 2: Extracted website_data keys: {list(website_data.keys()) if website_data else 'None'}")
|
||||
logger.info(f"🔍 Step 2: website_data.website: {website_data.get('website') if website_data else 'None'}")
|
||||
logger.info(f"🔍 Step 2: website_data.analysis: {bool(website_data.get('analysis')) if website_data else 'None'}")
|
||||
if website_data.get('analysis'):
|
||||
logger.info(f"🔍 Step 2: analysis keys: {list(website_data['analysis'].keys()) if isinstance(website_data.get('analysis'), dict) else 'Not dict'}")
|
||||
if website_data:
|
||||
try:
|
||||
saved = db_service.save_website_analysis(user_id, website_data, db)
|
||||
saved = self._save_website_analysis(user_id, website_data, db)
|
||||
if saved:
|
||||
logger.info(f"✅ Saved website analysis for user {user_id}")
|
||||
else:
|
||||
# This should not happen anymore since save_website_analysis now raises exceptions
|
||||
raise Exception("Website analysis save returned False")
|
||||
|
||||
# Trigger Advertools persona augmentation (Phase 1)
|
||||
try:
|
||||
from services.scheduler import get_scheduler
|
||||
|
||||
website_url = website_data.get('website') or website_data.get('website_url')
|
||||
if website_url:
|
||||
scheduler = get_scheduler()
|
||||
# Schedule content audit for persona augmentation
|
||||
scheduler.schedule_one_time_task(
|
||||
func=scheduler.execute_task_by_type,
|
||||
run_date=datetime.utcnow() + timedelta(seconds=10), # Start in 10s
|
||||
job_id=f"advertools_persona_augmentation_{user_id}",
|
||||
kwargs={
|
||||
"task_type": "advertools_intelligence",
|
||||
"user_id": user_id,
|
||||
"payload": {
|
||||
"type": "content_audit",
|
||||
"website_url": website_url
|
||||
}
|
||||
}
|
||||
)
|
||||
logger.info(f"🚀 Triggered Advertools persona augmentation for {website_url}")
|
||||
except Exception as sched_err:
|
||||
logger.error(f"Failed to trigger Advertools augmentation: {sched_err}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ BLOCKING ERROR: Failed to save website analysis: {str(e)}")
|
||||
raise HTTPException(
|
||||
@@ -261,15 +579,38 @@ class StepManagementService:
|
||||
logger.info(f"🔍 Step 3: Raw request_data keys: {list(request_data.keys()) if request_data else 'None'}")
|
||||
logger.info(f"🔍 Step 3: Extracted research_data keys: {list(research_data.keys()) if research_data else 'None'}")
|
||||
if research_data:
|
||||
# Note: Competitor data is saved separately via discover-competitors endpoint
|
||||
# This saves research preferences (content_types, target_audience, etc.)
|
||||
try:
|
||||
saved = db_service.save_research_preferences(user_id, research_data, db)
|
||||
saved = self._save_research_preferences(user_id, research_data, db)
|
||||
if saved:
|
||||
logger.info(f"✅ Saved research preferences for user {user_id}")
|
||||
else:
|
||||
# This should not happen anymore since save_research_preferences now raises exceptions
|
||||
raise Exception("Research preferences save returned False")
|
||||
|
||||
# Also save competitors if present
|
||||
competitors = research_data.get('competitors')
|
||||
if competitors:
|
||||
industry_context = research_data.get('industryContext') or research_data.get('industry_context')
|
||||
logger.info(f"🔍 Step 3: Found {len(competitors)} competitors to save")
|
||||
self._save_competitor_analysis(user_id, competitors, industry_context, db)
|
||||
|
||||
# Save social media presence if available (Update WebsiteAnalysis)
|
||||
social_media = research_data.get('social_media_accounts')
|
||||
if social_media:
|
||||
logger.info(f"🔍 Step 3: Found social media accounts to save")
|
||||
try:
|
||||
session = self._get_or_create_session(user_id, db)
|
||||
existing_analysis = db.query(WebsiteAnalysis).filter(
|
||||
WebsiteAnalysis.session_id == session.id
|
||||
).first()
|
||||
if existing_analysis:
|
||||
existing_analysis.social_media_presence = social_media
|
||||
existing_analysis.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
logger.info(f"✅ Updated social media presence for user {user_id}")
|
||||
else:
|
||||
logger.warning(f"⚠️ Could not save social media: WebsiteAnalysis not found for user {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Failed to save social media presence: {str(e)}")
|
||||
# Don't block completion for this, as it's secondary data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ BLOCKING ERROR: Failed to save research preferences: {str(e)}")
|
||||
raise HTTPException(
|
||||
@@ -284,12 +625,9 @@ class StepManagementService:
|
||||
logger.info(f"🔍 Step 4: Extracted persona_data keys: {list(persona_data.keys()) if persona_data else 'None'}")
|
||||
if persona_data:
|
||||
try:
|
||||
saved = db_service.save_persona_data(user_id, persona_data, db)
|
||||
saved = self._save_persona_data(user_id, persona_data, db)
|
||||
if saved:
|
||||
logger.info(f"✅ Saved persona data for user {user_id}")
|
||||
else:
|
||||
# This should not happen anymore since save_persona_data now raises exceptions
|
||||
raise Exception("Persona data save returned False")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ BLOCKING ERROR: Failed to save persona data: {str(e)}")
|
||||
raise HTTPException(
|
||||
@@ -298,10 +636,12 @@ class StepManagementService:
|
||||
) from e
|
||||
|
||||
# Persist current step and progress in DB
|
||||
db_service.update_step(user_id, step_number, db)
|
||||
from services.onboarding.progress_service import OnboardingProgressService
|
||||
progress_service = OnboardingProgressService()
|
||||
progress_service.update_step(user_id, step_number)
|
||||
try:
|
||||
progress_pct = min(100.0, round((step_number / 6) * 100))
|
||||
db_service.update_progress(user_id, float(progress_pct), db)
|
||||
progress_service.update_progress(user_id, float(progress_pct))
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to update progress: {e}")
|
||||
|
||||
@@ -309,6 +649,10 @@ class StepManagementService:
|
||||
if save_errors:
|
||||
logger.warning(f"⚠️ Step {step_number} completed but some data save operations failed: {save_errors}")
|
||||
|
||||
# Refresh SSOT (Canonical Profile) - non-blocking try/except inside method
|
||||
if not save_errors:
|
||||
await self.integration_service.refresh_integrated_data(user_id, db)
|
||||
|
||||
logger.info(f"[complete_step] Step {step_number} persisted to DB for user {user_id}")
|
||||
return {
|
||||
"message": "Step completed successfully",
|
||||
@@ -327,6 +671,7 @@ class StepManagementService:
|
||||
async def skip_step(self, step_number: int, current_user: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Skip a step (for optional steps)."""
|
||||
try:
|
||||
from services.onboarding.api_key_manager import get_onboarding_progress_for_user
|
||||
user_id = str(current_user.get('id'))
|
||||
progress = get_onboarding_progress_for_user(user_id)
|
||||
step = progress.get_step_data(step_number)
|
||||
|
||||
@@ -69,7 +69,7 @@ def get_persona_service() -> PersonaAnalysisService:
|
||||
"""Get the persona analysis service instance."""
|
||||
return PersonaAnalysisService()
|
||||
|
||||
async def generate_persona(user_id: int, request: PersonaGenerationRequest):
|
||||
async def generate_persona(user_id: str, request: PersonaGenerationRequest):
|
||||
"""Generate a new writing persona from onboarding data."""
|
||||
try:
|
||||
logger.info(f"Generating persona for user {user_id}")
|
||||
@@ -302,10 +302,10 @@ async def generate_platform_persona(user_id: str, platform: str, db_session):
|
||||
|
||||
# Import services
|
||||
from services.persona_data_service import PersonaDataService
|
||||
from services.onboarding.database_service import OnboardingDatabaseService
|
||||
from api.content_planning.services.content_strategy.onboarding import OnboardingDataIntegrationService
|
||||
|
||||
persona_data_service = PersonaDataService(db_session=db_session)
|
||||
onboarding_service = OnboardingDatabaseService(db=db_session)
|
||||
integration_service = OnboardingDataIntegrationService()
|
||||
|
||||
# Get core persona data
|
||||
persona_data = persona_data_service.get_user_persona_data(user_id)
|
||||
@@ -316,14 +316,16 @@ async def generate_platform_persona(user_id: str, platform: str, db_session):
|
||||
if not core_persona:
|
||||
raise HTTPException(status_code=404, detail="Core persona data is empty")
|
||||
|
||||
# Get onboarding data for context
|
||||
onboarding_session = onboarding_service.get_session_by_user(user_id)
|
||||
# Get onboarding data for context using SSOT
|
||||
integrated_data = integration_service.get_integrated_data_sync(user_id, db_session)
|
||||
onboarding_session = integrated_data.get('onboarding_session')
|
||||
|
||||
if not onboarding_session:
|
||||
raise HTTPException(status_code=404, detail="Onboarding session not found")
|
||||
|
||||
# Get website analysis for context
|
||||
website_analysis = onboarding_service.get_website_analysis(user_id)
|
||||
research_prefs = onboarding_service.get_research_preferences(user_id)
|
||||
website_analysis = integrated_data.get('website_analysis', {})
|
||||
research_prefs = integrated_data.get('research_preferences', {})
|
||||
|
||||
onboarding_data = {
|
||||
"website_url": website_analysis.get('website_url', '') if website_analysis else '',
|
||||
@@ -456,7 +458,7 @@ async def validate_persona_generation_readiness(user_id: int):
|
||||
logger.error(f"Error validating persona generation readiness: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to validate readiness: {str(e)}")
|
||||
|
||||
async def generate_persona_preview(user_id: int):
|
||||
async def generate_persona_preview(user_id: str):
|
||||
"""Generate a preview of what the persona would look like without saving."""
|
||||
try:
|
||||
persona_service = get_persona_service()
|
||||
@@ -758,4 +760,4 @@ async def optimize_facebook_persona(
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to optimize Facebook persona: {str(e)}"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -44,9 +44,10 @@ router = APIRouter(prefix="/api/personas", tags=["personas"])
|
||||
@router.post("/generate")
|
||||
async def generate_persona_endpoint(
|
||||
request: PersonaGenerationRequest,
|
||||
user_id: int = Query(1, description="User ID")
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""Generate a new writing persona from onboarding data."""
|
||||
user_id = str(current_user.get('id'))
|
||||
return await generate_persona(user_id, request)
|
||||
|
||||
@router.get("/user")
|
||||
@@ -256,4 +257,4 @@ async def check_facebook_persona_endpoint(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Check if Facebook persona exists for user."""
|
||||
return await check_facebook_persona(user_id, db)
|
||||
return await check_facebook_persona(user_id, db)
|
||||
|
||||
@@ -12,12 +12,15 @@ from services.story_writer.audio_generation_service import StoryAudioGenerationS
|
||||
# parents[0] = backend/api/podcast/
|
||||
# parents[1] = backend/api/
|
||||
# parents[2] = backend/
|
||||
BASE_DIR = Path(__file__).resolve().parents[2] # backend/
|
||||
PODCAST_AUDIO_DIR = (BASE_DIR / "podcast_audio").resolve()
|
||||
# parents[3] = root/
|
||||
ROOT_DIR = Path(__file__).resolve().parents[3] # root/
|
||||
DATA_MEDIA_DIR = ROOT_DIR / "data" / "media"
|
||||
|
||||
PODCAST_AUDIO_DIR = (DATA_MEDIA_DIR / "podcast_audio").resolve()
|
||||
PODCAST_AUDIO_DIR.mkdir(parents=True, exist_ok=True)
|
||||
PODCAST_IMAGES_DIR = (BASE_DIR / "podcast_images").resolve()
|
||||
PODCAST_IMAGES_DIR = (DATA_MEDIA_DIR / "podcast_images").resolve()
|
||||
PODCAST_IMAGES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
PODCAST_VIDEOS_DIR = (BASE_DIR / "podcast_videos").resolve()
|
||||
PODCAST_VIDEOS_DIR = (DATA_MEDIA_DIR / "podcast_videos").resolve()
|
||||
PODCAST_VIDEOS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Video subdirectory
|
||||
|
||||
@@ -76,20 +76,22 @@ async def analyze_research_intent(
|
||||
|
||||
if request.use_persona or request.use_competitor_data:
|
||||
from services.research.research_persona_service import ResearchPersonaService
|
||||
from services.onboarding.database_service import OnboardingDatabaseService
|
||||
from api.content_planning.services.content_strategy.onboarding import OnboardingDataIntegrationService
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# Get database session
|
||||
db = next(get_db())
|
||||
try:
|
||||
persona_service = ResearchPersonaService(db)
|
||||
onboarding_service = OnboardingDatabaseService(db=db)
|
||||
integration_service = OnboardingDataIntegrationService()
|
||||
|
||||
if request.use_persona:
|
||||
research_persona = persona_service.get_or_generate(user_id)
|
||||
|
||||
if request.use_competitor_data:
|
||||
competitor_data = onboarding_service.get_competitor_analysis(user_id, db)
|
||||
# Use SSOT integration service
|
||||
integrated_data = integration_service.get_integrated_data_sync(user_id, db)
|
||||
competitor_data = integrated_data.get('competitor_analysis', [])
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@ from pydantic import BaseModel
|
||||
|
||||
from middleware.auth_middleware import get_current_user
|
||||
from services.user_api_key_context import get_exa_key, get_gemini_key, get_tavily_key
|
||||
from services.onboarding.database_service import OnboardingDatabaseService
|
||||
from services.onboarding.progress_service import get_onboarding_progress_service
|
||||
from services.onboarding.progress_service import OnboardingProgressService
|
||||
from services.database import get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from services.research.research_persona_service import ResearchPersonaService
|
||||
from services.research.research_persona_scheduler import schedule_research_persona_generation
|
||||
from models.research_persona_models import ResearchPersona
|
||||
from api.content_planning.services.content_strategy.onboarding import OnboardingDataIntegrationService
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
@@ -129,8 +129,6 @@ async def get_persona_defaults(
|
||||
# Return minimal defaults - but onboarding guarantees this won't happen
|
||||
return PersonaDefaults()
|
||||
|
||||
db_service = OnboardingDatabaseService(db=db)
|
||||
|
||||
# Phase 2: First check if research persona exists (cached only - don't generate here)
|
||||
# Generation happens in ResearchEngine.research() on first use
|
||||
research_persona = None
|
||||
@@ -178,36 +176,27 @@ async def get_persona_defaults(
|
||||
provider_recommendations=getattr(research_persona, 'provider_recommendations', {}),
|
||||
)
|
||||
|
||||
# Fallback to core persona from onboarding (guaranteed to exist after onboarding)
|
||||
persona_data = db_service.get_persona_data(user_id, db)
|
||||
industry = None
|
||||
target_audience = None
|
||||
|
||||
if persona_data:
|
||||
core_persona = persona_data.get('corePersona') or persona_data.get('core_persona')
|
||||
if core_persona:
|
||||
industry = core_persona.get('industry')
|
||||
target_audience = core_persona.get('target_audience')
|
||||
|
||||
# Fallback to website analysis if core persona doesn't have industry
|
||||
if not industry:
|
||||
website_analysis = db_service.get_website_analysis(user_id, db)
|
||||
if website_analysis:
|
||||
target_audience_data = website_analysis.get('target_audience', {})
|
||||
if isinstance(target_audience_data, dict):
|
||||
industry = target_audience_data.get('industry_focus')
|
||||
demographics = target_audience_data.get('demographics')
|
||||
if demographics and not target_audience:
|
||||
target_audience = demographics if isinstance(demographics, str) else str(demographics)
|
||||
|
||||
# Phase 2: Never return "General" - use sensible defaults from onboarding or fallback
|
||||
# Since onboarding is mandatory, we should always have real data
|
||||
if not industry:
|
||||
industry = "Technology" # Safe default for content creators
|
||||
logger.warning(f"[ResearchConfig] No industry found for user {user_id}, using default")
|
||||
if not target_audience:
|
||||
target_audience = "Professionals" # Safe default
|
||||
logger.warning(f"[ResearchConfig] No target_audience found for user {user_id}, using default")
|
||||
# Use SSOT Integration Service to get canonical profile
|
||||
integration_service = OnboardingDataIntegrationService()
|
||||
integrated_data = integration_service.get_integrated_data_sync(user_id, db)
|
||||
canonical_profile = integrated_data.get('canonical_profile', {})
|
||||
|
||||
industry = canonical_profile.get('industry')
|
||||
target_audience_raw = canonical_profile.get('target_audience')
|
||||
|
||||
if isinstance(target_audience_raw, list):
|
||||
target_audience = ", ".join(str(item) for item in target_audience_raw if item is not None)
|
||||
elif isinstance(target_audience_raw, dict):
|
||||
target_audience = target_audience_raw.get('description') or target_audience_raw.get('label') or str(target_audience_raw)
|
||||
else:
|
||||
target_audience = target_audience_raw
|
||||
|
||||
if not industry or industry == "General":
|
||||
industry = "Technology"
|
||||
logger.warning(f"[ResearchConfig] No industry found in canonical profile for user {user_id}, using default")
|
||||
if not target_audience or target_audience == "General":
|
||||
target_audience = "Professionals and content consumers"
|
||||
logger.warning(f"[ResearchConfig] No target_audience found in canonical profile for user {user_id}, using default")
|
||||
|
||||
# Suggest domains based on industry
|
||||
suggested_domains = _get_domain_suggestions(industry)
|
||||
@@ -377,39 +366,21 @@ async def get_research_config(
|
||||
|
||||
# Get persona defaults
|
||||
logger.debug(f"[ResearchConfig] Getting persona defaults for user {user_id}")
|
||||
db_service = OnboardingDatabaseService(db=db)
|
||||
|
||||
# Try to get persona data first (most reliable source for industry/target_audience)
|
||||
try:
|
||||
persona_data = db_service.get_persona_data(user_id, db)
|
||||
except Exception as e:
|
||||
logger.error(f"[ResearchConfig] Error getting persona data for user {user_id}: {e}", exc_info=True)
|
||||
persona_data = None
|
||||
# Use SSOT Integration Service
|
||||
integration_service = OnboardingDataIntegrationService()
|
||||
integrated_data = integration_service.get_integrated_data_sync(user_id, db)
|
||||
canonical_profile = integrated_data.get('canonical_profile', {})
|
||||
|
||||
industry = 'General'
|
||||
target_audience = 'General'
|
||||
industry = canonical_profile.get('industry') or 'General'
|
||||
target_audience_raw = canonical_profile.get('target_audience')
|
||||
|
||||
if persona_data:
|
||||
core_persona = persona_data.get('corePersona') or persona_data.get('core_persona')
|
||||
if core_persona:
|
||||
if core_persona.get('industry'):
|
||||
industry = core_persona['industry']
|
||||
if core_persona.get('target_audience'):
|
||||
target_audience = core_persona['target_audience']
|
||||
|
||||
# Fallback to website analysis if persona data doesn't have industry info
|
||||
if industry == 'General':
|
||||
website_analysis = db_service.get_website_analysis(user_id, db)
|
||||
if website_analysis:
|
||||
target_audience_data = website_analysis.get('target_audience', {})
|
||||
if isinstance(target_audience_data, dict):
|
||||
# Extract from target_audience JSON field
|
||||
industry_focus = target_audience_data.get('industry_focus')
|
||||
if industry_focus:
|
||||
industry = industry_focus
|
||||
demographics = target_audience_data.get('demographics')
|
||||
if demographics:
|
||||
target_audience = demographics if isinstance(demographics, str) else str(demographics)
|
||||
if isinstance(target_audience_raw, list):
|
||||
target_audience = ", ".join(str(item) for item in target_audience_raw if item is not None)
|
||||
elif isinstance(target_audience_raw, dict):
|
||||
target_audience = target_audience_raw.get('description') or target_audience_raw.get('label') or str(target_audience_raw)
|
||||
else:
|
||||
target_audience = target_audience_raw or 'General'
|
||||
|
||||
persona_defaults = PersonaDefaults(
|
||||
industry=industry,
|
||||
@@ -422,7 +393,7 @@ async def get_research_config(
|
||||
onboarding_completed = False
|
||||
try:
|
||||
logger.debug(f"[ResearchConfig] Checking onboarding status for user {user_id}")
|
||||
progress_service = get_onboarding_progress_service()
|
||||
progress_service = OnboardingProgressService()
|
||||
onboarding_status = progress_service.get_onboarding_status(user_id)
|
||||
onboarding_completed = onboarding_status.get('is_completed', False)
|
||||
logger.info(
|
||||
@@ -466,8 +437,10 @@ async def get_research_config(
|
||||
if onboarding_completed and not research_persona:
|
||||
try:
|
||||
# Check if persona data exists (to ensure we have data to generate from)
|
||||
db_service = OnboardingDatabaseService(db=db)
|
||||
persona_data = db_service.get_persona_data(user_id, db)
|
||||
integration_service = OnboardingDataIntegrationService()
|
||||
integrated_data = integration_service.get_integrated_data_sync(user_id, db)
|
||||
persona_data = integrated_data.get('persona_data', {})
|
||||
|
||||
if persona_data and (persona_data.get('corePersona') or persona_data.get('platformPersonas') or
|
||||
persona_data.get('core_persona') or persona_data.get('platform_personas')):
|
||||
# Schedule persona generation (20 minutes from now)
|
||||
@@ -559,12 +532,16 @@ async def get_competitor_analysis(
|
||||
logger.error(f"[ResearchConfig] Database session is None for user {user_id}")
|
||||
raise HTTPException(status_code=500, detail="Database session not available")
|
||||
|
||||
db_service = OnboardingDatabaseService(db=db)
|
||||
# Use SSOT Integration Service
|
||||
integration_service = OnboardingDataIntegrationService()
|
||||
integrated_data = integration_service.get_integrated_data_sync(user_id, db)
|
||||
|
||||
onboarding_session = integrated_data.get('onboarding_session')
|
||||
|
||||
# Get onboarding session - using same pattern as onboarding completion check
|
||||
print(f"[COMPETITOR_ANALYSIS] Looking up onboarding session for user_id={user_id} (Clerk ID)")
|
||||
session = db_service.get_session_by_user(user_id, db)
|
||||
if not session:
|
||||
|
||||
if not onboarding_session:
|
||||
print(f"[COMPETITOR_ANALYSIS] ❌ WARNING: No onboarding session found for user_id={user_id}")
|
||||
logger.warning(f"[ResearchConfig] No onboarding session found for user {user_id}")
|
||||
return CompetitorAnalysisResponse(
|
||||
@@ -572,30 +549,31 @@ async def get_competitor_analysis(
|
||||
error="No onboarding session found. Please complete onboarding first."
|
||||
)
|
||||
|
||||
print(f"[COMPETITOR_ANALYSIS] ✅ Found onboarding session: id={session.id}, user_id={session.user_id}, current_step={session.current_step}")
|
||||
print(f"[COMPETITOR_ANALYSIS] ✅ Found onboarding session: id={onboarding_session.get('id')}, user_id={onboarding_session.get('user_id')}, current_step={onboarding_session.get('current_step')}")
|
||||
|
||||
# Check if step 3 is completed - same pattern as elsewhere (check current_step >= 3 or research_preferences exists)
|
||||
research_preferences = db_service.get_research_preferences(user_id, db)
|
||||
print(f"[COMPETITOR_ANALYSIS] Step check: current_step={session.current_step}, research_preferences exists={research_preferences is not None}")
|
||||
if not research_preferences and session.current_step < 3:
|
||||
print(f"[COMPETITOR_ANALYSIS] ❌ Step 3 not completed for user_id={user_id} (current_step={session.current_step})")
|
||||
logger.info(f"[ResearchConfig] Step 3 not completed for user {user_id} (current_step={session.current_step})")
|
||||
research_preferences = integrated_data.get('research_preferences')
|
||||
current_step = onboarding_session.get('current_step', 0)
|
||||
|
||||
print(f"[COMPETITOR_ANALYSIS] Step check: current_step={current_step}, research_preferences exists={research_preferences is not None}")
|
||||
if not research_preferences and current_step < 3:
|
||||
print(f"[COMPETITOR_ANALYSIS] ❌ Step 3 not completed for user_id={user_id} (current_step={current_step})")
|
||||
logger.info(f"[ResearchConfig] Step 3 not completed for user {user_id} (current_step={current_step})")
|
||||
return CompetitorAnalysisResponse(
|
||||
success=False,
|
||||
error="Onboarding step 3 (Competitor Analysis) is not completed. Please complete onboarding step 3 first."
|
||||
)
|
||||
|
||||
print(f"[COMPETITOR_ANALYSIS] ✅ Step 3 is completed (current_step={session.current_step} or research_preferences exists)")
|
||||
print(f"[COMPETITOR_ANALYSIS] ✅ Step 3 is completed (current_step={current_step} or research_preferences exists)")
|
||||
|
||||
# Try Method 1: Get competitor data from CompetitorAnalysis table using OnboardingDatabaseService
|
||||
# This follows the same pattern as get_website_analysis()
|
||||
print(f"[COMPETITOR_ANALYSIS] 🔍 Method 1: Querying CompetitorAnalysis table using OnboardingDatabaseService...")
|
||||
# Try Method 1: Get competitor data from SSOT (Integration Service)
|
||||
print(f"[COMPETITOR_ANALYSIS] 🔍 Method 1: Querying via OnboardingDataIntegrationService...")
|
||||
try:
|
||||
competitors = db_service.get_competitor_analysis(user_id, db)
|
||||
competitors = integrated_data.get('competitor_analysis', [])
|
||||
|
||||
if competitors:
|
||||
print(f"[COMPETITOR_ANALYSIS] ✅ Found {len(competitors)} competitor records from CompetitorAnalysis table")
|
||||
logger.info(f"[ResearchConfig] Found {len(competitors)} competitors from CompetitorAnalysis table for user {user_id}")
|
||||
print(f"[COMPETITOR_ANALYSIS] ✅ Found {len(competitors)} competitor records from SSOT")
|
||||
logger.info(f"[ResearchConfig] Found {len(competitors)} competitors from SSOT for user {user_id}")
|
||||
|
||||
# Map competitor fields to match frontend expectations
|
||||
mapped_competitors = []
|
||||
@@ -621,13 +599,13 @@ async def get_competitor_analysis(
|
||||
analysis_timestamp=None
|
||||
)
|
||||
else:
|
||||
print(f"[COMPETITOR_ANALYSIS] ⚠️ No competitor records found in CompetitorAnalysis table for user_id={user_id}")
|
||||
print(f"[COMPETITOR_ANALYSIS] ⚠️ No competitor records found in SSOT for user_id={user_id}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[COMPETITOR_ANALYSIS] ❌ EXCEPTION in Method 1: {e}")
|
||||
import traceback
|
||||
print(f"[COMPETITOR_ANALYSIS] Traceback:\n{traceback.format_exc()}")
|
||||
logger.warning(f"[ResearchConfig] Could not retrieve competitor data from CompetitorAnalysis table: {e}", exc_info=True)
|
||||
logger.warning(f"[ResearchConfig] Could not retrieve competitor data from SSOT: {e}", exc_info=True)
|
||||
|
||||
# Try Method 2: Get data from Step3ResearchService (which accesses step_data)
|
||||
# This is where step3_research_service._store_research_data() saves the data
|
||||
@@ -734,18 +712,21 @@ async def refresh_competitor_analysis(
|
||||
if not db:
|
||||
raise HTTPException(status_code=500, detail="Database session not available")
|
||||
|
||||
db_service = OnboardingDatabaseService(db=db)
|
||||
# Use SSOT Integration Service
|
||||
integration_service = OnboardingDataIntegrationService()
|
||||
integrated_data = integration_service.get_integrated_data_sync(user_id, db)
|
||||
|
||||
onboarding_session = integrated_data.get('onboarding_session')
|
||||
|
||||
# Get onboarding session
|
||||
session = db_service.get_session_by_user(user_id, db)
|
||||
if not session:
|
||||
if not onboarding_session:
|
||||
return CompetitorAnalysisResponse(
|
||||
success=False,
|
||||
error="No onboarding session found. Please complete onboarding first."
|
||||
)
|
||||
|
||||
# Get website URL from website analysis
|
||||
website_analysis = db_service.get_website_analysis(user_id, db)
|
||||
website_analysis = integrated_data.get('website_analysis') or {}
|
||||
if not website_analysis or not website_analysis.get('website_url'):
|
||||
return CompetitorAnalysisResponse(
|
||||
success=False,
|
||||
@@ -760,8 +741,8 @@ async def refresh_competitor_analysis(
|
||||
)
|
||||
|
||||
# Get industry context from research preferences or persona
|
||||
research_prefs = db_service.get_research_preferences(user_id, db) or {}
|
||||
persona_data = db_service.get_persona_data(user_id, db) or {}
|
||||
research_prefs = integrated_data.get('research_preferences') or {}
|
||||
persona_data = integrated_data.get('persona_data') or {}
|
||||
core_persona = persona_data.get('corePersona') or persona_data.get('core_persona') or {}
|
||||
industry_context = core_persona.get('industry') or research_prefs.get('industry') or None
|
||||
|
||||
@@ -778,8 +759,10 @@ async def refresh_competitor_analysis(
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
# Get the updated competitor data from database
|
||||
competitors = db_service.get_competitor_analysis(user_id, db)
|
||||
# Get the updated competitor data from SSOT (Integration Service)
|
||||
# Re-fetch integrated data to get the latest updates
|
||||
integrated_data = integration_service.get_integrated_data_sync(user_id, db)
|
||||
competitors = integrated_data.get('competitor_analysis', [])
|
||||
|
||||
if competitors:
|
||||
# Map competitor fields
|
||||
|
||||
@@ -19,7 +19,7 @@ from models.monitoring_models import TaskExecutionLog, MonitoringTask
|
||||
from models.scheduler_models import SchedulerEventLog
|
||||
from models.oauth_token_monitoring_models import OAuthTokenMonitoringTask
|
||||
from models.platform_insights_monitoring_models import PlatformInsightsTask, PlatformInsightsExecutionLog
|
||||
from models.website_analysis_monitoring_models import WebsiteAnalysisTask, WebsiteAnalysisExecutionLog
|
||||
from models.website_analysis_monitoring_models import WebsiteAnalysisTask, WebsiteAnalysisExecutionLog, DeepWebsiteCrawlTask
|
||||
|
||||
router = APIRouter(prefix="/api/scheduler", tags=["scheduler-dashboard"])
|
||||
|
||||
@@ -271,6 +271,43 @@ async def get_scheduler_dashboard(
|
||||
formatted_jobs.append(job_info)
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading platform insights tasks: {e}", exc_info=True)
|
||||
|
||||
# Load deep website crawl tasks
|
||||
try:
|
||||
crawl_tasks = db.query(DeepWebsiteCrawlTask).filter(
|
||||
DeepWebsiteCrawlTask.status.in_(['active', 'retry'])
|
||||
).all()
|
||||
|
||||
# Filter by user if user_id_str is provided
|
||||
if user_id_str:
|
||||
crawl_tasks = [t for t in crawl_tasks if t.user_id == user_id_str]
|
||||
|
||||
for task in crawl_tasks:
|
||||
try:
|
||||
user_job_store = get_user_job_store_name(task.user_id, db)
|
||||
except Exception as e:
|
||||
user_job_store = 'default'
|
||||
logger.debug(f"Could not get job store for user {task.user_id}: {e}")
|
||||
|
||||
# Format as recurring weekly job
|
||||
job_info = {
|
||||
'id': f"deep_website_crawl_{task.user_id}_{task.id}",
|
||||
'trigger_type': 'CronTrigger', # Weekly recurring
|
||||
'next_run_time': task.next_execution.isoformat() if task.next_execution else None,
|
||||
'user_id': task.user_id,
|
||||
'job_store': 'default',
|
||||
'user_job_store': user_job_store,
|
||||
'function_name': 'deep_website_crawl_executor.execute_task',
|
||||
'website_url': task.website_url,
|
||||
'task_id': task.id,
|
||||
'is_database_task': True,
|
||||
'frequency': 'Weekly',
|
||||
'task_category': 'deep_website_crawl'
|
||||
}
|
||||
|
||||
formatted_jobs.append(job_info)
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading deep website crawl tasks: {e}", exc_info=True)
|
||||
|
||||
# Get active strategies count
|
||||
active_strategies = stats.get('active_strategies_count', 0)
|
||||
|
||||
@@ -14,9 +14,16 @@ from services.onboarding.api_key_manager import APIKeyManager
|
||||
from services.validation import check_all_api_keys
|
||||
from services.seo_analyzer import ComprehensiveSEOAnalyzer, SEOAnalysisResult, SEOAnalysisService
|
||||
from services.user_data_service import UserDataService
|
||||
from services.database import get_db_session
|
||||
from services.database import get_db_session, get_session_for_user
|
||||
from services.seo import SEODashboardService
|
||||
from middleware.auth_middleware import get_current_user
|
||||
from services.llm_providers.main_text_generation import llm_text_gen
|
||||
from api.content_planning.services.content_strategy.onboarding import OnboardingDataIntegrationService
|
||||
from models.onboarding import SEOPageAudit, WebsiteAnalysis, OnboardingSession
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
# Phase 2B: Import semantic monitoring
|
||||
from services.intelligence.monitoring.semantic_dashboard import RealTimeSemanticMonitor, SemanticHealthMetric
|
||||
|
||||
# Initialize the SEO analyzer
|
||||
seo_analyzer = ComprehensiveSEOAnalyzer()
|
||||
@@ -64,6 +71,9 @@ class SEOAnalysisRequest(BaseModel):
|
||||
url: str
|
||||
target_keywords: Optional[List[str]] = None
|
||||
|
||||
class AnalyzeURLsRequest(BaseModel):
|
||||
urls: List[str]
|
||||
|
||||
class SEOAnalysisResponse(BaseModel):
|
||||
url: str
|
||||
timestamp: datetime
|
||||
@@ -239,12 +249,105 @@ def generate_ai_insights(metrics: Dict[str, Any], platforms: Dict[str, Any]) ->
|
||||
|
||||
return insights
|
||||
|
||||
from services.seo.deep_competitor_analysis_service import DeepCompetitorAnalysisService
|
||||
|
||||
# API Endpoints
|
||||
async def run_strategic_insights(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Manually trigger AI-Powered Competitive Insights (Weekly Strategy Brief).
|
||||
"""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
db_session = get_db_session(user_id)
|
||||
|
||||
if not db_session:
|
||||
raise HTTPException(status_code=500, detail="Database connection unavailable")
|
||||
|
||||
try:
|
||||
# 1. Get Website Analysis (with fallback)
|
||||
website_analysis_data = None
|
||||
analysis_id = None
|
||||
|
||||
# Try SSOT first
|
||||
integration_service = OnboardingDataIntegrationService()
|
||||
integrated_data = integration_service.get_integrated_data_sync(user_id, db_session)
|
||||
if integrated_data and integrated_data.get("website_analysis"):
|
||||
website_analysis_data = integrated_data.get("website_analysis")
|
||||
analysis_id = website_analysis_data.get("id")
|
||||
|
||||
# Fallback: Find latest WebsiteAnalysis across sessions
|
||||
if not website_analysis_data:
|
||||
latest_analysis = db_session.query(WebsiteAnalysis).join(
|
||||
OnboardingSession, WebsiteAnalysis.session_id == OnboardingSession.id
|
||||
).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
).order_by(WebsiteAnalysis.updated_at.desc()).first()
|
||||
|
||||
if latest_analysis:
|
||||
# Convert to dict
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
website_analysis_data = jsonable_encoder(latest_analysis)
|
||||
analysis_id = latest_analysis.id
|
||||
|
||||
if not website_analysis_data:
|
||||
raise HTTPException(status_code=400, detail="No website analysis found. Please complete Onboarding Step 2.")
|
||||
|
||||
# 2. Get Competitors
|
||||
competitors = []
|
||||
if integrated_data:
|
||||
competitors = integrated_data.get("competitor_analysis", [])
|
||||
|
||||
if not competitors:
|
||||
# Fallback to research preferences
|
||||
research_prefs = integrated_data.get("research_preferences", {})
|
||||
competitors = research_prefs.get("competitors", [])
|
||||
|
||||
if not competitors:
|
||||
raise HTTPException(status_code=400, detail="No competitors found. Please complete Onboarding Step 3.")
|
||||
|
||||
# 3. Run Analysis
|
||||
service = DeepCompetitorAnalysisService()
|
||||
report = await service.generate_weekly_strategy_brief(
|
||||
user_id=user_id,
|
||||
website_analysis=website_analysis_data,
|
||||
competitors=competitors
|
||||
)
|
||||
|
||||
# 4. Persist to History
|
||||
if analysis_id:
|
||||
wa = db_session.query(WebsiteAnalysis).filter(WebsiteAnalysis.id == analysis_id).first()
|
||||
if wa:
|
||||
history = wa.strategic_insights_history or []
|
||||
# Ensure history is a list
|
||||
if not isinstance(history, list):
|
||||
history = []
|
||||
|
||||
# Prepend new report
|
||||
history.insert(0, report)
|
||||
|
||||
# Keep last 52 weeks
|
||||
wa.strategic_insights_history = history[:52]
|
||||
flag_modified(wa, "strategic_insights_history")
|
||||
db_session.commit()
|
||||
|
||||
return report
|
||||
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
except HTTPException as he:
|
||||
raise he
|
||||
except Exception as e:
|
||||
logger.error(f"Error running strategic insights: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to run analysis: {str(e)}")
|
||||
|
||||
async def get_seo_dashboard_data(current_user: dict = Depends(get_current_user)) -> SEODashboardData:
|
||||
"""Get comprehensive SEO dashboard data."""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
db_session = get_db_session()
|
||||
db_session = get_db_session(user_id)
|
||||
|
||||
if not db_session:
|
||||
logger.error("No database session available")
|
||||
@@ -278,7 +381,7 @@ async def get_seo_health_score(current_user: dict = Depends(get_current_user)) -
|
||||
"""Get current SEO health score."""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
db_session = get_db_session()
|
||||
db_session = get_db_session(user_id)
|
||||
|
||||
if not db_session:
|
||||
raise HTTPException(status_code=500, detail="Database connection unavailable")
|
||||
@@ -299,7 +402,7 @@ async def get_seo_metrics(current_user: dict = Depends(get_current_user)) -> Dic
|
||||
"""Get SEO metrics."""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
db_session = get_db_session()
|
||||
db_session = get_db_session(user_id)
|
||||
|
||||
if not db_session:
|
||||
raise HTTPException(status_code=500, detail="Database connection unavailable")
|
||||
@@ -322,7 +425,7 @@ async def get_platform_status(
|
||||
"""Get platform connection status."""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
db_session = get_db_session()
|
||||
db_session = get_db_session(user_id)
|
||||
|
||||
if not db_session:
|
||||
logger.error("No database session available")
|
||||
@@ -347,7 +450,7 @@ async def get_ai_insights(current_user: dict = Depends(get_current_user)) -> Lis
|
||||
"""Get AI-generated insights."""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
db_session = get_db_session()
|
||||
db_session = get_db_session(user_id)
|
||||
|
||||
if not db_session:
|
||||
raise HTTPException(status_code=500, detail="Database connection unavailable")
|
||||
@@ -368,6 +471,59 @@ async def seo_dashboard_health_check():
|
||||
"""Health check for SEO dashboard."""
|
||||
return {"status": "healthy", "service": "SEO Dashboard API"}
|
||||
|
||||
# Phase 2B: Semantic health monitoring endpoint
|
||||
async def get_semantic_health(current_user: dict = Depends(get_current_user)) -> SemanticHealthMetric:
|
||||
"""
|
||||
Get real-time semantic health metrics for the user's content and competitors.
|
||||
This endpoint provides Phase 2B semantic intelligence monitoring data.
|
||||
|
||||
Returns:
|
||||
SemanticHealthMetric with current health status, score, and recommendations
|
||||
"""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
|
||||
# Initialize semantic monitor for this user
|
||||
semantic_monitor = RealTimeSemanticMonitor(user_id)
|
||||
|
||||
# Get current semantic health (will use cache if available)
|
||||
semantic_health = await semantic_monitor.check_semantic_health(user_id)
|
||||
|
||||
logger.info(f"[Semantic Health API] Retrieved health data for user {user_id}: {semantic_health.status} (score: {semantic_health.value:.2f})")
|
||||
|
||||
return semantic_health
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Semantic Health API] Error retrieving semantic health for user: {e}")
|
||||
# Return a default healthy state with warning message
|
||||
return SemanticHealthMetric(
|
||||
metric_name="semantic_health",
|
||||
value=0.5,
|
||||
threshold=0.6,
|
||||
status="warning",
|
||||
timestamp=datetime.utcnow().isoformat(),
|
||||
description="Semantic monitoring temporarily unavailable",
|
||||
recommendations=["Please try again later", "Check system status"]
|
||||
)
|
||||
|
||||
|
||||
async def get_semantic_cache_stats(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""
|
||||
Get statistics for the semantic cache.
|
||||
"""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
# Initialize semantic monitor to access its cache manager
|
||||
semantic_monitor = RealTimeSemanticMonitor(user_id)
|
||||
return await semantic_monitor.get_cache_stats()
|
||||
except Exception as e:
|
||||
logger.error(f"[Semantic Cache API] Error retrieving cache stats: {e}")
|
||||
return {
|
||||
"error": "Failed to retrieve cache statistics",
|
||||
"hit_rate": 0.0,
|
||||
"memory_usage_mb": 0.0
|
||||
}
|
||||
|
||||
# New comprehensive SEO analysis endpoints
|
||||
async def analyze_seo_comprehensive(request: SEOAnalysisRequest) -> SEOAnalysisResponse:
|
||||
"""
|
||||
@@ -650,6 +806,107 @@ async def batch_analyze_urls(urls: List[str]) -> Dict[str, Any]:
|
||||
detail=f"Error in batch analysis: {str(e)}"
|
||||
)
|
||||
|
||||
async def analyze_urls_ai(request: AnalyzeURLsRequest, current_user: dict) -> Dict[str, Any]:
|
||||
"""Run AI analysis on selected URLs."""
|
||||
user_id = str(current_user.get('id'))
|
||||
db_session = get_db_session()
|
||||
results = []
|
||||
|
||||
try:
|
||||
for url in request.urls:
|
||||
# Check if audit exists
|
||||
audit = db_session.query(SEOPageAudit).filter(
|
||||
SEOPageAudit.user_id == user_id,
|
||||
SEOPageAudit.page_url == url
|
||||
).first()
|
||||
|
||||
if not audit:
|
||||
results.append({"url": url, "status": "skipped", "reason": "No audit found"})
|
||||
continue
|
||||
|
||||
# Prepare Prompt
|
||||
# We use the existing audit data (algorithmic) to feed the AI
|
||||
audit_summary = {
|
||||
"score": audit.overall_score,
|
||||
"issues": audit.issues,
|
||||
"warnings": audit.warnings
|
||||
}
|
||||
|
||||
prompt = f"""
|
||||
As an expert SEO consultant, analyze these technical audit results for the page: {url}
|
||||
|
||||
AUDIT DATA:
|
||||
{json.dumps(audit_summary, default=str)[:3000]}
|
||||
|
||||
TASK:
|
||||
Provide 3 specific, high-impact AI recommendations to improve this page's SEO.
|
||||
Focus on content relevance, user intent, and semantic SEO, which the algorithmic audit might miss.
|
||||
|
||||
OUTPUT JSON format:
|
||||
[
|
||||
{{ "category": "Content|Technical|UX", "recommendation": "...", "impact": "High|Medium", "effort": "Low|Medium" }}
|
||||
]
|
||||
"""
|
||||
|
||||
try:
|
||||
ai_response = llm_text_gen(prompt, user_id=user_id)
|
||||
# Parse JSON
|
||||
import re
|
||||
cleaned = ai_response.strip().replace("```json", "").replace("```", "")
|
||||
# Simple regex to find the JSON array if extra text exists
|
||||
match = re.search(r'\[.*\]', cleaned, re.DOTALL)
|
||||
if match:
|
||||
cleaned = match.group(0)
|
||||
|
||||
recommendations = json.loads(cleaned)
|
||||
|
||||
# Update audit
|
||||
current_recs = audit.recommendations or []
|
||||
if isinstance(current_recs, list):
|
||||
# Tag new ones
|
||||
for r in recommendations:
|
||||
r['source'] = 'ai_on_demand'
|
||||
current_recs.extend(recommendations)
|
||||
audit.recommendations = current_recs
|
||||
|
||||
audit.last_analyzed_at = datetime.utcnow()
|
||||
results.append({"url": url, "status": "success"})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"AI Analysis failed for {url}: {e}")
|
||||
results.append({"url": url, "status": "failed", "error": str(e)})
|
||||
|
||||
db_session.commit()
|
||||
return {"results": results}
|
||||
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
async def get_analyzed_pages(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""Get list of pages that have been analyzed by AI."""
|
||||
user_id = str(current_user.get('id'))
|
||||
db_session = get_db_session()
|
||||
|
||||
try:
|
||||
audits = db_session.query(SEOPageAudit).filter(
|
||||
SEOPageAudit.user_id == user_id
|
||||
).all()
|
||||
|
||||
results = []
|
||||
for audit in audits:
|
||||
if audit.recommendations:
|
||||
results.append({
|
||||
"url": audit.page_url,
|
||||
"analyzed_at": audit.last_analyzed_at,
|
||||
"score": audit.overall_score,
|
||||
"recommendations_count": len(audit.recommendations)
|
||||
})
|
||||
|
||||
return {"results": results}
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
|
||||
# New SEO Dashboard Endpoints with Real Data
|
||||
|
||||
async def get_seo_dashboard_overview(
|
||||
@@ -659,7 +916,7 @@ async def get_seo_dashboard_overview(
|
||||
"""Get comprehensive SEO dashboard overview with real GSC/Bing data."""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
db_session = get_db_session()
|
||||
db_session = get_session_for_user(user_id)
|
||||
|
||||
if not db_session:
|
||||
logger.error("No database session available")
|
||||
@@ -715,7 +972,7 @@ async def get_bing_raw_data(
|
||||
"""Get raw Bing data for the specified site."""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
db_session = get_db_session()
|
||||
db_session = get_db_session(user_id)
|
||||
|
||||
if not db_session:
|
||||
logger.error("No database session available")
|
||||
@@ -743,7 +1000,7 @@ async def get_competitive_insights(
|
||||
"""Get competitive insights from onboarding step 3 data."""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
db_session = get_db_session()
|
||||
db_session = get_db_session(user_id)
|
||||
|
||||
if not db_session:
|
||||
logger.error("No database session available")
|
||||
@@ -764,6 +1021,153 @@ async def get_competitive_insights(
|
||||
logger.error(f"Error getting competitive insights: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to get competitive insights")
|
||||
|
||||
|
||||
async def get_deep_competitor_analysis(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
site_url: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
db_session = get_session_for_user(user_id)
|
||||
|
||||
if not db_session:
|
||||
logger.error("No database session available")
|
||||
raise HTTPException(status_code=500, detail="Database connection failed")
|
||||
|
||||
try:
|
||||
integration_service = OnboardingDataIntegrationService()
|
||||
integrated = integration_service.get_integrated_data_sync(user_id, db_session)
|
||||
deep = integrated.get("deep_competitor_analysis") if isinstance(integrated, dict) else None
|
||||
return deep or {
|
||||
"status": "not_available",
|
||||
"last_run": None,
|
||||
"report": None
|
||||
}
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting deep competitor analysis: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to get deep competitor analysis")
|
||||
|
||||
|
||||
async def run_strategic_insights(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> Dict[str, Any]:
|
||||
"""Run AI-powered strategic insights analysis manually."""
|
||||
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 failed")
|
||||
|
||||
try:
|
||||
integration_service = OnboardingDataIntegrationService()
|
||||
integrated = integration_service.get_integrated_data_sync(user_id, db_session)
|
||||
|
||||
website_analysis_data = integrated.get("website_analysis")
|
||||
logger.info(f"Integrated data for user {user_id}: website_analysis found? {bool(website_analysis_data)}")
|
||||
|
||||
# Fallback: If not found in integrated data (e.g. strict session mismatch), find latest analysis for user
|
||||
if not website_analysis_data:
|
||||
logger.info(f"Attempting fallback for user {user_id}")
|
||||
# Find latest WebsiteAnalysis for this user across all sessions
|
||||
latest_analysis = db_session.query(WebsiteAnalysis).join(
|
||||
OnboardingSession, WebsiteAnalysis.session_id == OnboardingSession.id
|
||||
).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
).order_by(WebsiteAnalysis.updated_at.desc()).first()
|
||||
|
||||
if latest_analysis:
|
||||
logger.info(f"Found fallback WebsiteAnalysis {latest_analysis.id} for user {user_id}")
|
||||
website_analysis_data = latest_analysis.to_dict()
|
||||
# Ensure ID is present for updates
|
||||
website_analysis_data['id'] = latest_analysis.id
|
||||
else:
|
||||
logger.warning(f"Fallback failed for user {user_id}. No WebsiteAnalysis found.")
|
||||
|
||||
if not website_analysis_data:
|
||||
raise HTTPException(status_code=400, detail="Website analysis (Step 2) not found. Please complete onboarding.")
|
||||
|
||||
research_prefs = integrated.get("research_preferences")
|
||||
competitors = (research_prefs.get("competitors") if isinstance(research_prefs, dict) else None)
|
||||
|
||||
if not competitors:
|
||||
# Try competitor_analysis as fallback
|
||||
competitors = integrated.get("competitor_analysis") or []
|
||||
|
||||
if not competitors:
|
||||
raise HTTPException(status_code=400, detail="No competitors found. Please add competitors in Step 3.")
|
||||
|
||||
from services.seo.deep_competitor_analysis_service import DeepCompetitorAnalysisService
|
||||
analysis_service = DeepCompetitorAnalysisService()
|
||||
|
||||
logger.info(f"Running manual strategic insights for user {user_id}")
|
||||
report = await analysis_service.generate_weekly_strategy_brief(
|
||||
user_id=user_id,
|
||||
website_analysis=website_analysis_data if isinstance(website_analysis_data, dict) else {},
|
||||
competitors=competitors if isinstance(competitors, list) else []
|
||||
)
|
||||
|
||||
# Find the WebsiteAnalysis record to persist history
|
||||
analysis_id = website_analysis_data.get('id') if isinstance(website_analysis_data, dict) else None
|
||||
if analysis_id:
|
||||
website_analysis = db_session.query(WebsiteAnalysis).filter(WebsiteAnalysis.id == analysis_id).first()
|
||||
|
||||
if website_analysis:
|
||||
history = website_analysis.strategic_insights_history or []
|
||||
if not isinstance(history, list):
|
||||
history = []
|
||||
|
||||
# Append new report at the beginning (latest first)
|
||||
history.insert(0, report)
|
||||
# Keep last 52 weeks (1 year)
|
||||
website_analysis.strategic_insights_history = history[:52]
|
||||
flag_modified(website_analysis, "strategic_insights_history")
|
||||
db_session.commit()
|
||||
logger.info(f"Persisted strategic insight for user {user_id} to history")
|
||||
|
||||
return {"success": True, "report": report}
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error running strategic insights: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to run strategic insights: {str(e)}")
|
||||
|
||||
|
||||
async def get_strategic_insights_history(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> Dict[str, Any]:
|
||||
"""Fetch the history of strategic insights for the user."""
|
||||
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 failed")
|
||||
|
||||
try:
|
||||
integration_service = OnboardingDataIntegrationService()
|
||||
integrated = integration_service.get_integrated_data_sync(user_id, db_session)
|
||||
|
||||
website_analysis = integrated.get("website_analysis")
|
||||
if not website_analysis or not isinstance(website_analysis, dict):
|
||||
return {"history": []}
|
||||
|
||||
history = website_analysis.get("strategic_insights_history") or []
|
||||
return {"history": history}
|
||||
finally:
|
||||
db_session.close()
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching strategic insights history: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to fetch strategic insights history")
|
||||
|
||||
async def refresh_analytics_data(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
site_url: Optional[str] = None
|
||||
@@ -771,7 +1175,7 @@ async def refresh_analytics_data(
|
||||
"""Refresh analytics data by invalidating cache and fetching fresh data."""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
db_session = get_db_session()
|
||||
db_session = get_db_session(user_id)
|
||||
|
||||
if not db_session:
|
||||
logger.error("No database session available")
|
||||
@@ -849,4 +1253,4 @@ def _convert_platforms(platform_data: Dict[str, Any]) -> Dict[str, PlatformStatu
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error converting platforms: {e}")
|
||||
return {}
|
||||
return {}
|
||||
|
||||
@@ -26,7 +26,7 @@ from services.story_writer.audio_generation_service import StoryAudioGenerationS
|
||||
from utils.asset_tracker import save_asset_to_library
|
||||
|
||||
from ..utils.auth import require_authenticated_user
|
||||
from ..utils.media_utils import resolve_media_file
|
||||
from ..utils.media_utils import resolve_media_file, resolve_story_media_path
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
@@ -57,6 +57,7 @@ async def generate_scene_images(
|
||||
width=request.width or 1024,
|
||||
height=request.height or 1024,
|
||||
model=request.model,
|
||||
db=db,
|
||||
)
|
||||
|
||||
image_models: List[StoryImageResult] = [
|
||||
|
||||
@@ -94,7 +94,7 @@ async def animate_scene_preview(
|
||||
request.image_url,
|
||||
)
|
||||
|
||||
image_bytes = load_story_image_bytes(request.image_url)
|
||||
image_bytes = load_story_image_bytes(request.image_url, user_id=user_id)
|
||||
if not image_bytes:
|
||||
scene_logger.warning("[AnimateScene] Missing image bytes for user=%s scene=%s", user_id, request.scene_number)
|
||||
raise HTTPException(status_code=404, detail="Scene image not found. Generate images first.")
|
||||
@@ -114,29 +114,35 @@ async def animate_scene_preview(
|
||||
duration=duration,
|
||||
)
|
||||
|
||||
base_dir = Path(__file__).parent.parent.parent.parent
|
||||
ai_video_dir = base_dir / "story_videos" / AI_VIDEO_SUBDIR
|
||||
ai_video_dir.mkdir(parents=True, exist_ok=True)
|
||||
video_service = StoryVideoGenerationService(output_dir=str(ai_video_dir))
|
||||
|
||||
save_result = video_service.save_scene_video(
|
||||
video_bytes=animation_result["video_bytes"],
|
||||
scene_number=request.scene_number,
|
||||
user_id=user_id,
|
||||
)
|
||||
video_filename = save_result["video_filename"]
|
||||
video_url = _build_authenticated_media_url(
|
||||
request_obj, f"/api/story/videos/ai/{video_filename}"
|
||||
)
|
||||
|
||||
usage_info = track_video_usage(
|
||||
user_id=user_id,
|
||||
provider=animation_result["provider"],
|
||||
model_name=animation_result["model_name"],
|
||||
prompt=animation_result["prompt"],
|
||||
video_bytes=animation_result["video_bytes"],
|
||||
cost_override=animation_result["cost"],
|
||||
)
|
||||
# Save video asset to library
|
||||
db = next(get_db())
|
||||
try:
|
||||
video_service = StoryVideoGenerationService(output_dir=str(ai_video_dir))
|
||||
|
||||
save_result = video_service.save_scene_video(
|
||||
video_bytes=animation_result["video_bytes"],
|
||||
scene_number=request.scene_number,
|
||||
user_id=user_id,
|
||||
db=db
|
||||
)
|
||||
video_filename = save_result["video_filename"]
|
||||
video_url = _build_authenticated_media_url(
|
||||
request_obj, f"/api/story/videos/ai/{video_filename}"
|
||||
)
|
||||
|
||||
usage_info = track_video_usage(
|
||||
user_id=user_id,
|
||||
provider=animation_result["provider"],
|
||||
model_name=animation_result["model_name"],
|
||||
prompt=animation_result["prompt"],
|
||||
video_bytes=animation_result["video_bytes"],
|
||||
cost_override=animation_result["cost"],
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to track usage for generated video: {e}")
|
||||
# Don't fail the request if tracking fails, just log it
|
||||
pass
|
||||
|
||||
if usage_info:
|
||||
scene_logger.warning(
|
||||
"[AnimateScene] Video usage tracked user=%s: %s → %s / %s (cost +$%.2f, total=$%.2f)",
|
||||
|
||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
@@ -88,8 +88,8 @@ async def generate_story_video(
|
||||
valid_scenes: List[Dict[str, Any]] = []
|
||||
|
||||
# Resolve video/audio directories
|
||||
base_dir = Path(__file__).parent.parent.parent.parent
|
||||
ai_video_dir = (base_dir / "story_videos" / "AI_Videos").resolve()
|
||||
base_dir = Path(__file__).resolve().parents[4]
|
||||
ai_video_dir = (base_dir / "data" / "media" / "story_videos" / "AI_Videos").resolve()
|
||||
|
||||
video_urls = request.video_urls or [None] * len(request.scenes)
|
||||
ai_audio_urls = request.ai_audio_urls or [None] * len(request.scenes)
|
||||
|
||||
@@ -7,15 +7,91 @@ from urllib.parse import urlparse
|
||||
from fastapi import HTTPException, status
|
||||
from loguru import logger
|
||||
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parents[3] # backend/
|
||||
STORY_IMAGES_DIR = (BASE_DIR / "story_images").resolve()
|
||||
STORY_IMAGES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
STORY_AUDIO_DIR = (BASE_DIR / "story_audio").resolve()
|
||||
STORY_AUDIO_DIR.mkdir(parents=True, exist_ok=True)
|
||||
from services.database import get_db
|
||||
from services.user_workspace_manager import UserWorkspaceManager
|
||||
|
||||
|
||||
def load_story_image_bytes(image_url: str) -> Optional[bytes]:
|
||||
BASE_DIR = Path(__file__).resolve().parents[4] # root/
|
||||
DATA_MEDIA_DIR = BASE_DIR / "workspace" / "media"
|
||||
|
||||
STORY_IMAGES_DIR = (DATA_MEDIA_DIR / "story_images").resolve()
|
||||
# STORY_IMAGES_DIR.mkdir(parents=True, exist_ok=True) # Disabled global creation
|
||||
|
||||
STORY_AUDIO_DIR = (DATA_MEDIA_DIR / "story_audio").resolve()
|
||||
# STORY_AUDIO_DIR.mkdir(parents=True, exist_ok=True) # Disabled global creation
|
||||
|
||||
|
||||
def _get_user_media_path(user_id: str, media_type: str) -> Optional[Path]:
|
||||
"""Resolve user-specific media directory."""
|
||||
try:
|
||||
# We need a new session for this operation
|
||||
db_gen = get_db()
|
||||
db = next(db_gen)
|
||||
try:
|
||||
workspace_manager = UserWorkspaceManager(db)
|
||||
workspace = workspace_manager.get_user_workspace(user_id)
|
||||
if workspace:
|
||||
# media/story_images or media/story_audio
|
||||
subdir = "story_images" if media_type == "image" else "story_audio"
|
||||
path = Path(workspace['workspace_path']) / "media" / subdir
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
finally:
|
||||
# Ensure we close the session if it's not managed by dependency injection
|
||||
# Since get_db yields, we can't easily close it unless we manage the generator
|
||||
# But get_db uses SessionLocal() which should be closed.
|
||||
# However, get_db is a generator. We should really use a context manager or dependency.
|
||||
# Here we just took next(db), so it's an open session.
|
||||
# We should probably close it.
|
||||
# Actually, UserWorkspaceManager uses the passed db.
|
||||
# Let's assume standard usage pattern for manual DB access.
|
||||
pass
|
||||
# Note: The generator usage here is a bit tricky for cleanup.
|
||||
# Ideally we'd have a context manager.
|
||||
# For now, let's rely on garbage collection or explicit close if possible.
|
||||
# But SQLAlchemy sessions should be closed.
|
||||
# db.close() # valid if db is Session
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to resolve user workspace path for {user_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def resolve_story_media_path(filename: str, media_type: str, user_id: Optional[str] = None) -> Path:
|
||||
"""
|
||||
Resolve a story media file path, checking user workspace first then global directory.
|
||||
media_type: 'image' or 'audio'
|
||||
"""
|
||||
filename = filename.split("?")[0].strip()
|
||||
|
||||
# 1. Try user workspace
|
||||
if user_id:
|
||||
user_path = _get_user_media_path(user_id, media_type)
|
||||
if user_path:
|
||||
file_path = (user_path / filename).resolve()
|
||||
# Guard against traversal
|
||||
if str(file_path).startswith(str(user_path)) and file_path.exists():
|
||||
return file_path
|
||||
|
||||
# 2. Fallback to global directory
|
||||
base_dir = STORY_IMAGES_DIR if media_type == "image" else STORY_AUDIO_DIR
|
||||
file_path = (base_dir / filename).resolve()
|
||||
|
||||
if not str(file_path).startswith(str(base_dir)):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
|
||||
|
||||
if file_path.exists():
|
||||
return file_path
|
||||
|
||||
# 3. If not found, try alternate in global (legacy behavior support)
|
||||
alternate = _find_alternate_media_file(base_dir, filename)
|
||||
if alternate:
|
||||
logger.warning(f"[StoryWriter] Serving alternate media for {filename}: {alternate.name}")
|
||||
return alternate
|
||||
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"File not found: {filename}")
|
||||
|
||||
|
||||
def load_story_image_bytes(image_url: str, user_id: Optional[str] = None) -> Optional[bytes]:
|
||||
"""
|
||||
Resolve an authenticated story image URL (e.g., /api/story/images/<file>) to raw bytes.
|
||||
Returns None if the file cannot be located.
|
||||
@@ -35,22 +111,21 @@ def load_story_image_bytes(image_url: str) -> Optional[bytes]:
|
||||
if not filename:
|
||||
return None
|
||||
|
||||
file_path = (STORY_IMAGES_DIR / filename).resolve()
|
||||
if not str(file_path).startswith(str(STORY_IMAGES_DIR)):
|
||||
logger.error(f"[StoryWriter] Attempted path traversal when resolving image: {image_url}")
|
||||
# Try to resolve path using helper
|
||||
try:
|
||||
file_path = resolve_story_media_path(filename, "image", user_id)
|
||||
return file_path.read_bytes()
|
||||
except HTTPException:
|
||||
# Not found
|
||||
logger.warning(f"[StoryWriter] Referenced scene image not found: {filename}")
|
||||
return None
|
||||
|
||||
if not file_path.exists():
|
||||
logger.warning(f"[StoryWriter] Referenced scene image not found on disk: {file_path}")
|
||||
return None
|
||||
|
||||
return file_path.read_bytes()
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"[StoryWriter] Failed to load reference image for video gen: {exc}")
|
||||
return None
|
||||
|
||||
|
||||
def load_story_audio_bytes(audio_url: str) -> Optional[bytes]:
|
||||
def load_story_audio_bytes(audio_url: str, user_id: Optional[str] = None) -> Optional[bytes]:
|
||||
"""
|
||||
Resolve an authenticated story audio URL (e.g., /api/story/audio/<file>) to raw bytes.
|
||||
Returns None if the file cannot be located.
|
||||
@@ -70,16 +145,15 @@ def load_story_audio_bytes(audio_url: str) -> Optional[bytes]:
|
||||
if not filename:
|
||||
return None
|
||||
|
||||
file_path = (STORY_AUDIO_DIR / filename).resolve()
|
||||
if not str(file_path).startswith(str(STORY_AUDIO_DIR)):
|
||||
logger.error(f"[StoryWriter] Attempted path traversal when resolving audio: {audio_url}")
|
||||
# Try to resolve path using helper
|
||||
try:
|
||||
file_path = resolve_story_media_path(filename, "audio", user_id)
|
||||
return file_path.read_bytes()
|
||||
except HTTPException:
|
||||
# Not found
|
||||
logger.warning(f"[StoryWriter] Referenced scene audio not found: {filename}")
|
||||
return None
|
||||
|
||||
if not file_path.exists():
|
||||
logger.warning(f"[StoryWriter] Referenced scene audio not found on disk: {file_path}")
|
||||
return None
|
||||
|
||||
return file_path.read_bytes()
|
||||
except Exception as exc:
|
||||
logger.error(f"[StoryWriter] Failed to load reference audio for video gen: {exc}")
|
||||
return None
|
||||
|
||||
@@ -63,8 +63,17 @@ async def get_usage_alerts(
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting usage alerts: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
logger.error(f"Error getting usage alerts: {e}", exc_info=True)
|
||||
# Return empty alerts instead of 500
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"alerts": [],
|
||||
"total": 0,
|
||||
"unread_count": 0,
|
||||
"message": f"Error retrieving alerts: {str(e)}"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("/alerts/{alert_id}/mark-read")
|
||||
|
||||
@@ -164,7 +164,29 @@ async def get_dashboard_data(
|
||||
return response_payload
|
||||
except Exception as retry_err:
|
||||
logger.error(f"Schema fix and retry failed: {retry_err}")
|
||||
raise HTTPException(status_code=500, detail=f"Database schema error: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(retry_err),
|
||||
"data": {
|
||||
"current_usage": {"total_calls": 0, "total_cost": 0, "usage_status": "error", "provider_breakdown": {}},
|
||||
"trends": [],
|
||||
"limits": {"limits": {"monthly_cost": 0}},
|
||||
"alerts": [],
|
||||
"projections": {"projected_monthly_cost": 0, "cost_limit": 0, "projected_usage_percentage": 0},
|
||||
"summary": {"total_api_calls_this_month": 0, "total_cost_this_month": 0, "usage_status": "error", "unread_alerts": 0}
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(f"Error getting dashboard data: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"data": {
|
||||
"current_usage": {"total_calls": 0, "total_cost": 0, "usage_status": "error", "provider_breakdown": {}},
|
||||
"trends": [],
|
||||
"limits": {"limits": {"monthly_cost": 0}},
|
||||
"alerts": [],
|
||||
"projections": {"projected_monthly_cost": 0, "cost_limit": 0, "projected_usage_percentage": 0},
|
||||
"summary": {"total_api_calls_this_month": 0, "total_cost_this_month": 0, "usage_status": "error", "unread_alerts": 0}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,8 +115,15 @@ async def preflight_check(
|
||||
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:
|
||||
# Audio pricing is per character (every character is 1 token)
|
||||
cost = (pricing_info.get('cost_per_input_token', 0.0) or 0.0) * (op['tokens_requested'] / 1000.0)
|
||||
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:
|
||||
# Audio pricing is per character (every character is 1 token)
|
||||
cost = (pricing_info.get('cost_per_input_token', 0.0) or 0.0) * (op['tokens_requested'] / 1000.0)
|
||||
elif op['tokens_requested'] > 0:
|
||||
# Token-based cost estimation (rough estimate)
|
||||
cost = (pricing_info.get('cost_per_input_token', 0.0) or 0.0) * (op['tokens_requested'] / 1000)
|
||||
|
||||
@@ -12,6 +12,7 @@ import sqlite3
|
||||
from services.database import get_db
|
||||
from services.subscription import UsageTrackingService, PricingService
|
||||
from services.subscription.schema_utils import ensure_subscription_plan_columns
|
||||
from services.user_workspace_manager import UserWorkspaceManager
|
||||
from middleware.auth_middleware import get_current_user
|
||||
from models.subscription_models import (
|
||||
SubscriptionPlan, UserSubscription, UsageSummary,
|
||||
@@ -93,7 +94,23 @@ async def get_user_subscription(
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user subscription: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"data": {
|
||||
"subscription": None,
|
||||
"plan": {
|
||||
"id": "error_fallback",
|
||||
"name": "Error Fallback",
|
||||
"tier": "free",
|
||||
"price_monthly": 0,
|
||||
"description": "Unable to load subscription details",
|
||||
"is_free": True
|
||||
},
|
||||
"status": "error",
|
||||
"limits": {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/status/{user_id}")
|
||||
@@ -255,11 +272,29 @@ async def get_subscription_status(
|
||||
}
|
||||
}
|
||||
except Exception as retry_err:
|
||||
logger.error(f"Schema fix and retry failed: {retry_err}")
|
||||
raise HTTPException(status_code=500, detail=f"Database schema error: {str(e)}")
|
||||
logger.error(f"Schema fix and retry failed: {retry_err}", exc_info=True)
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"active": False,
|
||||
"plan": "none",
|
||||
"tier": "none",
|
||||
"can_use_api": False,
|
||||
"reason": f"Database schema error: {str(e)}"
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(f"Error getting subscription status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
logger.error(f"Error getting subscription status: {e}", exc_info=True)
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"active": False,
|
||||
"plan": "none",
|
||||
"tier": "none",
|
||||
"can_use_api": False,
|
||||
"reason": f"Failed to check subscription status: {str(e)}"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("/subscribe/{user_id}")
|
||||
@@ -383,6 +418,18 @@ async def subscribe_to_plan(
|
||||
auto_renew=True
|
||||
)
|
||||
db.add(subscription)
|
||||
|
||||
# Ensure user workspace exists for new subscribers
|
||||
# MOVED: Workspace creation is now handled exclusively in the onboarding flow
|
||||
# to prevent premature creation before plan selection/onboarding.
|
||||
# See onboarding_control_service.py
|
||||
# try:
|
||||
# logger.info(f"Creating workspace for new subscriber {user_id}")
|
||||
# workspace_manager = UserWorkspaceManager(db)
|
||||
# workspace_manager.create_user_workspace(user_id)
|
||||
# except Exception as ws_error:
|
||||
# logger.error(f"Failed to create workspace for new subscriber {user_id}: {ws_error}")
|
||||
# # Don't fail the subscription if workspace creation fails, but log it
|
||||
|
||||
db.commit()
|
||||
|
||||
@@ -491,6 +538,15 @@ async def subscribe_to_plan(
|
||||
except Exception as reset_err:
|
||||
logger.error(f" ❌ Failed to reset usage after subscribe: {reset_err}", exc_info=True)
|
||||
|
||||
# Ensure user workspace is created/verified upon subscription
|
||||
try:
|
||||
workspace_manager = UserWorkspaceManager(db)
|
||||
workspace_manager.create_user_workspace(user_id)
|
||||
logger.info(f" ✅ User workspace verified/created for user {user_id}")
|
||||
except Exception as ws_err:
|
||||
# Log but don't fail the subscription response, as workspace can be created later
|
||||
logger.error(f" ⚠️ Failed to create user workspace during subscription: {ws_err}")
|
||||
|
||||
logger.info(f" ✅ Renewal completed: User {user_id} → {plan.name} ({billing_cycle})")
|
||||
logger.info("=" * 80)
|
||||
|
||||
|
||||
197
backend/api/today_workflow.py
Normal file
197
backend/api/today_workflow.py
Normal file
@@ -0,0 +1,197 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import Any, Dict, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from middleware.auth_middleware import get_current_user
|
||||
from services.database import get_db
|
||||
from services.today_workflow_service import get_or_create_daily_workflow_plan, update_task_status
|
||||
from models.daily_workflow_models import DailyWorkflowPlan, DailyWorkflowTask
|
||||
import asyncio
|
||||
from services.intelligence.txtai_service import TxtaiIntelligenceService
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/today-workflow", tags=["Today Workflow"])
|
||||
|
||||
async def _index_tasks_to_sif(user_id: str, date: str, tasks: list[dict], label: str):
|
||||
svc = TxtaiIntelligenceService(user_id)
|
||||
items = []
|
||||
for t in tasks:
|
||||
task_id = t.get("id")
|
||||
pillar_id = t.get("pillarId")
|
||||
status = t.get("status")
|
||||
title = t.get("title")
|
||||
description = t.get("description")
|
||||
text = f"[{pillar_id}] {title}\n{description}\nstatus={status}"
|
||||
metadata = {
|
||||
"type": "daily_workflow_task",
|
||||
"date": date,
|
||||
"label": label,
|
||||
"pillar_id": pillar_id,
|
||||
"status": status,
|
||||
"implemented": status == "completed",
|
||||
"dismissed": status == "skipped",
|
||||
"task_id": task_id,
|
||||
}
|
||||
items.append((f"{label}_task:{user_id}:{date}:{task_id}", text, metadata))
|
||||
try:
|
||||
await svc.index_content(items)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def get_today_workflow(
|
||||
date: Optional[str] = None,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Dict[str, Any]:
|
||||
user_id = str(current_user.get("id"))
|
||||
plan, created = get_or_create_daily_workflow_plan(db, user_id, date=date)
|
||||
|
||||
tasks = (
|
||||
db.query(DailyWorkflowTask)
|
||||
.filter(DailyWorkflowTask.plan_id == plan.id, DailyWorkflowTask.user_id == user_id)
|
||||
.order_by(DailyWorkflowTask.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
response_tasks = []
|
||||
for t in tasks:
|
||||
response_tasks.append(
|
||||
{
|
||||
"id": str(t.id),
|
||||
"pillarId": t.pillar_id,
|
||||
"title": t.title,
|
||||
"description": t.description,
|
||||
"status": "skipped" if t.status == "dismissed" else t.status,
|
||||
"priority": t.priority,
|
||||
"estimatedTime": t.estimated_time,
|
||||
"dependencies": t.dependencies or [],
|
||||
"actionUrl": t.action_url,
|
||||
"actionType": t.action_type,
|
||||
"metadata": t.metadata_json or {},
|
||||
"enabled": bool(t.enabled),
|
||||
}
|
||||
)
|
||||
|
||||
total = len(response_tasks)
|
||||
completed = len([t for t in response_tasks if t["status"] in ("completed", "skipped")])
|
||||
current_index = 0
|
||||
for i, task in enumerate(response_tasks):
|
||||
if task["status"] not in ("completed", "skipped"):
|
||||
current_index = i
|
||||
break
|
||||
current_index = i
|
||||
|
||||
workflow_status = "not_started"
|
||||
if completed > 0 and completed < total:
|
||||
workflow_status = "in_progress"
|
||||
elif total > 0 and completed == total:
|
||||
workflow_status = "completed"
|
||||
|
||||
total_estimated = int(sum(int(t.get("estimatedTime") or 0) for t in response_tasks))
|
||||
|
||||
if created:
|
||||
asyncio.create_task(_index_tasks_to_sif(user_id, plan.date, response_tasks, label="today"))
|
||||
try:
|
||||
from datetime import date as date_type, timedelta
|
||||
|
||||
y_str = (date_type.fromisoformat(plan.date) - timedelta(days=1)).isoformat()
|
||||
y_plan = (
|
||||
db.query(DailyWorkflowPlan)
|
||||
.filter(DailyWorkflowPlan.user_id == user_id, DailyWorkflowPlan.date == y_str)
|
||||
.first()
|
||||
)
|
||||
if y_plan:
|
||||
y_tasks = (
|
||||
db.query(DailyWorkflowTask)
|
||||
.filter(DailyWorkflowTask.plan_id == y_plan.id, DailyWorkflowTask.user_id == user_id)
|
||||
.order_by(DailyWorkflowTask.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
y_response = []
|
||||
for t in y_tasks:
|
||||
y_response.append(
|
||||
{
|
||||
"id": str(t.id),
|
||||
"pillarId": t.pillar_id,
|
||||
"title": t.title,
|
||||
"description": t.description,
|
||||
"status": "skipped" if t.status == "dismissed" else t.status,
|
||||
}
|
||||
)
|
||||
asyncio.create_task(_index_tasks_to_sif(user_id, y_str, y_response, label="yesterday"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"workflow": {
|
||||
"id": f"daily-{user_id}-{plan.date}",
|
||||
"date": plan.date,
|
||||
"userId": user_id,
|
||||
"tasks": response_tasks,
|
||||
"currentTaskIndex": current_index,
|
||||
"completedTasks": completed,
|
||||
"totalTasks": total,
|
||||
"workflowStatus": workflow_status,
|
||||
"totalEstimatedTime": total_estimated,
|
||||
"actualTimeSpent": 0,
|
||||
},
|
||||
"plan": {
|
||||
"id": plan.id,
|
||||
"date": plan.date,
|
||||
"source": plan.source,
|
||||
"created_at": plan.created_at.isoformat() if plan.created_at else None,
|
||||
"updated_at": plan.updated_at.isoformat() if plan.updated_at else None,
|
||||
},
|
||||
},
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"user_id": user_id,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/status")
|
||||
async def set_task_status(
|
||||
task_id: int,
|
||||
body: Dict[str, Any],
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Dict[str, Any]:
|
||||
user_id = str(current_user.get("id"))
|
||||
status = body.get("status")
|
||||
if not status:
|
||||
raise HTTPException(status_code=400, detail="status is required")
|
||||
completion_notes = body.get("completion_notes")
|
||||
|
||||
task = update_task_status(db, user_id, task_id, status=status, completion_notes=completion_notes)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
plan_for_date = db.query(DailyWorkflowPlan).filter(DailyWorkflowPlan.id == task.plan_id).first()
|
||||
plan_date = plan_for_date.date if plan_for_date and plan_for_date.date else ""
|
||||
task_payload = {
|
||||
"id": str(task.id),
|
||||
"pillarId": task.pillar_id,
|
||||
"title": task.title,
|
||||
"description": task.description,
|
||||
"status": "skipped" if task.status == "dismissed" else task.status,
|
||||
}
|
||||
asyncio.create_task(_index_tasks_to_sif(user_id, plan_date, [task_payload], label="today"))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"task": {
|
||||
"id": str(task.id),
|
||||
"pillarId": task.pillar_id,
|
||||
"status": "skipped" if task.status == "dismissed" else task.status,
|
||||
"decided_at": task.decided_at.isoformat() if task.decided_at else None,
|
||||
}
|
||||
},
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"user_id": user_id,
|
||||
}
|
||||
@@ -21,7 +21,7 @@ router = APIRouter(prefix="/api/wix", tags=["Wix Integration"])
|
||||
wix_service = WixService()
|
||||
|
||||
# Initialize Wix OAuth service for token storage
|
||||
wix_oauth_service = WixOAuthService(db_path=os.path.abspath("alwrity.db"))
|
||||
wix_oauth_service = WixOAuthService()
|
||||
|
||||
|
||||
class WixAuthRequest(BaseModel):
|
||||
|
||||
@@ -19,8 +19,9 @@ router = APIRouter(tags=["youtube-audio"])
|
||||
logger = get_service_logger("api.youtube.audio")
|
||||
|
||||
# Audio output directory
|
||||
base_dir = Path(__file__).parent.parent.parent.parent
|
||||
YOUTUBE_AUDIO_DIR = base_dir / "youtube_audio"
|
||||
# api/youtube/handlers/audio.py -> handlers -> youtube -> api -> backend -> root
|
||||
base_dir = Path(__file__).resolve().parents[4]
|
||||
YOUTUBE_AUDIO_DIR = base_dir / "workspace" / "media" / "youtube_audio"
|
||||
YOUTUBE_AUDIO_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Initialize audio service
|
||||
|
||||
@@ -19,8 +19,10 @@ router = APIRouter(prefix="/avatar", tags=["youtube-avatar"])
|
||||
logger = get_service_logger("api.youtube.avatar")
|
||||
|
||||
# Directories
|
||||
base_dir = Path(__file__).parent.parent.parent.parent
|
||||
YOUTUBE_AVATARS_DIR = base_dir / "youtube_avatars"
|
||||
# api/youtube/handlers/avatar.py -> handlers -> youtube -> api -> backend -> root
|
||||
base_dir = Path(__file__).parent.parent.parent.parent.parent
|
||||
DATA_MEDIA_DIR = base_dir / "data" / "media"
|
||||
YOUTUBE_AVATARS_DIR = DATA_MEDIA_DIR / "youtube_avatars"
|
||||
YOUTUBE_AVATARS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
|
||||
@@ -23,10 +23,12 @@ router = APIRouter(tags=["youtube-image"])
|
||||
logger = get_service_logger("api.youtube.image")
|
||||
|
||||
# Directories
|
||||
base_dir = Path(__file__).parent.parent.parent.parent
|
||||
YOUTUBE_IMAGES_DIR = base_dir / "youtube_images"
|
||||
# api/youtube/handlers/images.py -> handlers -> youtube -> api -> backend -> root
|
||||
base_dir = Path(__file__).parent.parent.parent.parent.parent
|
||||
DATA_MEDIA_DIR = base_dir / "data" / "media"
|
||||
YOUTUBE_IMAGES_DIR = DATA_MEDIA_DIR / "youtube_images"
|
||||
YOUTUBE_IMAGES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
YOUTUBE_AVATARS_DIR = base_dir / "youtube_avatars"
|
||||
YOUTUBE_AVATARS_DIR = DATA_MEDIA_DIR / "youtube_avatars"
|
||||
|
||||
# Thread pool for background image generation
|
||||
_image_executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="youtube_image")
|
||||
|
||||
@@ -25,6 +25,7 @@ from models.content_asset_models import AssetType, AssetSource
|
||||
from utils.logger_utils import get_service_logger
|
||||
from utils.asset_tracker import save_asset_to_library
|
||||
from services.story_writer.video_generation_service import StoryVideoGenerationService
|
||||
from services.user_workspace_manager import UserWorkspaceManager
|
||||
from .task_manager import task_manager
|
||||
from .handlers import avatar as avatar_handlers
|
||||
from .handlers import images as image_handlers
|
||||
@@ -34,13 +35,16 @@ router = APIRouter(prefix="/youtube", tags=["youtube"])
|
||||
logger = get_service_logger("api.youtube")
|
||||
|
||||
# Video output and image directories
|
||||
base_dir = Path(__file__).parent.parent.parent.parent
|
||||
YOUTUBE_VIDEO_DIR = base_dir / "youtube_videos"
|
||||
YOUTUBE_VIDEO_DIR.mkdir(parents=True, exist_ok=True)
|
||||
YOUTUBE_AVATARS_DIR = base_dir / "youtube_avatars"
|
||||
YOUTUBE_AVATARS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
YOUTUBE_IMAGES_DIR = base_dir / "youtube_images"
|
||||
YOUTUBE_IMAGES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
# api/youtube/router.py -> youtube -> api -> backend -> root
|
||||
base_dir = Path(__file__).resolve().parents[3]
|
||||
DATA_MEDIA_DIR = base_dir / "workspace" / "media"
|
||||
YOUTUBE_VIDEO_DIR = DATA_MEDIA_DIR / "youtube_videos"
|
||||
YOUTUBE_AVATARS_DIR = DATA_MEDIA_DIR / "youtube_avatars"
|
||||
YOUTUBE_IMAGES_DIR = DATA_MEDIA_DIR / "youtube_images"
|
||||
|
||||
# Ensure directories exist
|
||||
for directory in [YOUTUBE_VIDEO_DIR, YOUTUBE_AVATARS_DIR, YOUTUBE_IMAGES_DIR]:
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Include sub-routers for avatar, images, and audio
|
||||
router.include_router(avatar_handlers.router)
|
||||
@@ -820,6 +824,11 @@ def _execute_video_render_task(
|
||||
)
|
||||
return
|
||||
|
||||
# Create DB session for workspace resolution
|
||||
from services.database import get_db
|
||||
db_gen = get_db()
|
||||
db = next(db_gen)
|
||||
|
||||
try:
|
||||
task_manager.update_task_status(
|
||||
task_id, "processing", progress=5.0, message="Initializing render..."
|
||||
@@ -892,6 +901,7 @@ def _execute_video_render_task(
|
||||
resolution=resolution,
|
||||
generate_audio_enabled=True,
|
||||
voice_id=voice_id,
|
||||
db=db,
|
||||
)
|
||||
|
||||
scene_results.append(scene_result)
|
||||
@@ -899,35 +909,30 @@ def _execute_video_render_task(
|
||||
|
||||
# Save to asset library
|
||||
try:
|
||||
from services.database import get_db
|
||||
db = next(get_db())
|
||||
try:
|
||||
save_asset_to_library(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
asset_type="video",
|
||||
source_module="youtube_creator",
|
||||
filename=scene_result["video_filename"],
|
||||
file_url=scene_result["video_url"],
|
||||
file_path=scene_result["video_path"],
|
||||
file_size=scene_result["file_size"],
|
||||
mime_type="video/mp4",
|
||||
title=f"YouTube Scene {scene_num}: {scene.get('title', 'Untitled')}",
|
||||
description=f"Scene {scene_num} from YouTube video",
|
||||
prompt=scene.get("visual_prompt", ""),
|
||||
tags=["youtube_creator", "video", "scene", f"scene_{scene_num}", resolution],
|
||||
provider="wavespeed",
|
||||
model="alibaba/wan-2.5/text-to-video",
|
||||
cost=scene_result["cost"],
|
||||
asset_metadata={
|
||||
"scene_number": scene_num,
|
||||
"duration": scene_result["duration"],
|
||||
"resolution": resolution,
|
||||
"status": "completed"
|
||||
}
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
save_asset_to_library(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
asset_type="video",
|
||||
source_module="youtube_creator",
|
||||
filename=scene_result["video_filename"],
|
||||
file_url=scene_result["video_url"],
|
||||
file_path=scene_result["video_path"],
|
||||
file_size=scene_result["file_size"],
|
||||
mime_type="video/mp4",
|
||||
title=f"YouTube Scene {scene_num}: {scene.get('title', 'Untitled')}",
|
||||
description=f"Scene {scene_num} from YouTube video",
|
||||
prompt=scene.get("visual_prompt", ""),
|
||||
tags=["youtube_creator", "video", "scene", f"scene_{scene_num}", resolution],
|
||||
provider="wavespeed",
|
||||
model="alibaba/wan-2.5/text-to-video",
|
||||
cost=scene_result["cost"],
|
||||
asset_metadata={
|
||||
"scene_number": scene_num,
|
||||
"duration": scene_result["duration"],
|
||||
"resolution": resolution,
|
||||
"status": "completed"
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[YouTubeRenderer] Failed to save scene to library: {e}")
|
||||
|
||||
@@ -1070,6 +1075,7 @@ def _execute_video_render_task(
|
||||
resolution=resolution,
|
||||
combine_scenes=True,
|
||||
voice_id=voice_id,
|
||||
db=db,
|
||||
)
|
||||
|
||||
final_video_url = combined_result.get("final_video_url")
|
||||
@@ -1132,6 +1138,9 @@ def _execute_video_render_task(
|
||||
error=error_msg,
|
||||
message=f"Video rendering error: {error_msg}",
|
||||
)
|
||||
finally:
|
||||
if 'db' in locals():
|
||||
db.close()
|
||||
|
||||
|
||||
def _execute_scene_video_render_task(
|
||||
@@ -1156,6 +1165,11 @@ def _execute_scene_video_render_task(
|
||||
)
|
||||
return
|
||||
|
||||
# Create DB session for workspace resolution
|
||||
from services.database import get_db
|
||||
db_gen = get_db()
|
||||
db = next(db_gen)
|
||||
|
||||
try:
|
||||
task_manager.update_task_status(
|
||||
task_id, "processing", progress=5.0, message=f"Rendering scene {scene_num}..."
|
||||
@@ -1170,6 +1184,7 @@ def _execute_scene_video_render_task(
|
||||
resolution=resolution,
|
||||
generate_audio_enabled=generate_audio_enabled,
|
||||
voice_id=voice_id,
|
||||
db=db,
|
||||
)
|
||||
|
||||
total_cost = scene_result.get("cost", 0.0) or 0.0
|
||||
@@ -1229,6 +1244,9 @@ def _execute_scene_video_render_task(
|
||||
error=error_msg,
|
||||
message=f"Scene {scene_num} rendering error: {error_msg}",
|
||||
)
|
||||
finally:
|
||||
if 'db' in locals():
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/render/combine", response_model=CombineVideosResponse)
|
||||
@@ -1398,19 +1416,50 @@ def _execute_combine_video_task(
|
||||
logger.error(f"[YouTubeRenderer] Task {task_id} not found when combine task started.")
|
||||
return
|
||||
|
||||
base_dir = Path(__file__).parent.parent.parent.parent
|
||||
youtube_video_dir = base_dir / "youtube_videos"
|
||||
# Create DB session for workspace resolution
|
||||
from services.database import get_db
|
||||
from services.user_workspace_manager import UserWorkspaceManager
|
||||
|
||||
db_gen = get_db()
|
||||
db = next(db_gen)
|
||||
|
||||
try:
|
||||
task_manager.update_task_status(
|
||||
task_id, "processing", progress=5.0, message="Preparing to combine videos..."
|
||||
)
|
||||
|
||||
# Resolve user workspace directory
|
||||
workspace_manager = UserWorkspaceManager(db)
|
||||
workspace_info = workspace_manager.get_user_workspace(user_id)
|
||||
|
||||
if workspace_info and workspace_info.get('workspace_path'):
|
||||
user_video_dir = Path(workspace_info['workspace_path']) / "content" / "videos"
|
||||
if not user_video_dir.exists():
|
||||
user_video_dir.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
# Fallback to default directory
|
||||
base_dir = Path(__file__).parent.parent.parent.parent
|
||||
user_video_dir = base_dir / "youtube_videos"
|
||||
logger.warning(f"Workspace not found for user {user_id}, using default directory: {user_video_dir}")
|
||||
|
||||
# Fallback directory (legacy global directory) for backward compatibility
|
||||
base_dir = Path(__file__).parent.parent.parent.parent
|
||||
legacy_video_dir = base_dir / "youtube_videos"
|
||||
|
||||
# Resolve video paths from URLs
|
||||
video_paths: List[Path] = []
|
||||
for url in scene_video_urls:
|
||||
filename = Path(url).name
|
||||
video_path = youtube_video_dir / filename
|
||||
|
||||
# Check user directory first
|
||||
video_path = user_video_dir / filename
|
||||
|
||||
# If not found, check legacy directory
|
||||
if not video_path.exists():
|
||||
legacy_path = legacy_video_dir / filename
|
||||
if legacy_path.exists():
|
||||
video_path = legacy_path
|
||||
|
||||
if not video_path.exists():
|
||||
logger.error(f"[YouTubeRenderer] Video file not found for combine: {video_path}")
|
||||
raise HTTPException(
|
||||
@@ -1426,7 +1475,8 @@ def _execute_combine_video_task(
|
||||
task_id, "processing", progress=25.0, message="Combining scene videos..."
|
||||
)
|
||||
|
||||
video_service = StoryVideoGenerationService(output_dir=str(youtube_video_dir))
|
||||
# Use user video directory for output
|
||||
video_service = StoryVideoGenerationService(output_dir=str(user_video_dir))
|
||||
combined_result = video_service.generate_story_video(
|
||||
scenes=[
|
||||
{"scene_number": idx + 1, "title": f"Scene {idx + 1}"}
|
||||
@@ -1448,34 +1498,30 @@ def _execute_combine_video_task(
|
||||
final_url = combined_result["video_url"]
|
||||
file_size = combined_result.get("file_size", 0)
|
||||
|
||||
# Save to asset library
|
||||
# Save to asset library using existing db session
|
||||
try:
|
||||
db = next(get_db())
|
||||
try:
|
||||
save_asset_to_library(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
asset_type="video",
|
||||
source_module="youtube_creator",
|
||||
filename=Path(final_path).name,
|
||||
file_url=final_url,
|
||||
file_path=str(final_path),
|
||||
file_size=file_size,
|
||||
mime_type="video/mp4",
|
||||
title=title or "YouTube Video",
|
||||
description="Combined YouTube creator video",
|
||||
tags=["youtube_creator", "video", "combined", resolution],
|
||||
provider="wavespeed",
|
||||
model="alibaba/wan-2.5/text-to-video",
|
||||
cost=0.0,
|
||||
asset_metadata={
|
||||
"resolution": resolution,
|
||||
"status": "completed",
|
||||
"scene_count": len(video_paths),
|
||||
},
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
save_asset_to_library(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
asset_type="video",
|
||||
source_module="youtube_creator",
|
||||
filename=Path(final_path).name,
|
||||
file_url=final_url,
|
||||
file_path=str(final_path),
|
||||
file_size=file_size,
|
||||
mime_type="video/mp4",
|
||||
title=title or "YouTube Video",
|
||||
description="Combined YouTube creator video",
|
||||
tags=["youtube_creator", "video", "combined", resolution],
|
||||
provider="wavespeed",
|
||||
model="alibaba/wan-2.5/text-to-video",
|
||||
cost=0.0,
|
||||
asset_metadata={
|
||||
"resolution": resolution,
|
||||
"status": "completed",
|
||||
"scene_count": len(video_paths),
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[YouTubeRenderer] Failed to save combined video to asset library: {e}")
|
||||
|
||||
@@ -1516,6 +1562,9 @@ def _execute_combine_video_task(
|
||||
error=error_msg,
|
||||
message=f"Combine error: {error_msg}",
|
||||
)
|
||||
finally:
|
||||
if 'db' in locals():
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/estimate-cost", response_model=CostEstimateResponse)
|
||||
|
||||
Reference in New Issue
Block a user