AI Analysis and Content Strategy fixes. Enhanced Strategy Routes refactoring.

This commit is contained in:
ajaysi
2026-01-10 19:32:50 +05:30
parent 0b63ae7fc1
commit 8193cdba67
298 changed files with 45678 additions and 10952 deletions

View File

@@ -0,0 +1,9 @@
"""
Research API Handlers
Handler modules for research endpoints.
"""
from . import providers, research, intent, projects
__all__ = ["providers", "research", "intent", "projects"]

View File

@@ -0,0 +1,394 @@
"""
Intent-Driven Research Handler
Handles intent analysis and intent-driven research endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException
from typing import Dict, Any
from loguru import logger
import asyncio
from services.database import get_db
from services.research.core import (
ResearchEngine,
ResearchContext,
ResearchPersonalizationContext,
ResearchGoal,
ResearchDepth,
ProviderPreference,
)
from middleware.auth_middleware import get_current_user
from models.research_intent_models import (
ResearchIntent,
ResearchQuery,
ExpectedDeliverable,
)
from services.research.intent import (
ResearchIntentInference,
IntentQueryGenerator,
IntentAwareAnalyzer,
)
from ..models import (
AnalyzeIntentRequest,
AnalyzeIntentResponse,
IntentDrivenResearchRequest,
IntentDrivenResearchResponse,
)
from ..utils import (
map_purpose_to_goal,
map_depth_to_engine_depth,
map_provider_to_preference,
merge_trends_data,
)
router = APIRouter()
@router.post("/intent/analyze", response_model=AnalyzeIntentResponse)
async def analyze_research_intent(
request: AnalyzeIntentRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Analyze user input to understand research intent.
This endpoint uses AI to infer what the user really wants from their research:
- What questions need answering
- What deliverables they expect (statistics, quotes, case studies, etc.)
- What depth and focus is appropriate
The response includes quick options that can be shown in the UI for user confirmation.
"""
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="Invalid user ID")
logger.info(f"[Intent API] Analyzing intent for: {request.user_input[:50]}...")
# Get research persona if requested
research_persona = None
competitor_data = None
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 sqlalchemy.orm import Session
# Get database session
db = next(get_db())
try:
persona_service = ResearchPersonaService(db)
onboarding_service = OnboardingDatabaseService(db=db)
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)
finally:
db.close()
# Use Unified Research Analyzer (single AI call for intent + queries + params)
from services.research.intent.unified_research_analyzer import UnifiedResearchAnalyzer
analyzer = UnifiedResearchAnalyzer()
unified_result = await analyzer.analyze(
user_input=request.user_input,
keywords=request.keywords,
research_persona=research_persona,
competitor_data=competitor_data,
industry=research_persona.default_industry if research_persona else None,
target_audience=research_persona.default_target_audience if research_persona else None,
user_id=user_id,
user_provided_purpose=request.user_provided_purpose,
user_provided_content_output=request.user_provided_content_output,
user_provided_depth=request.user_provided_depth,
)
if not unified_result.get("success", False):
logger.warning("Unified analysis failed, using fallback")
# Extract results
intent = unified_result.get("intent")
queries = unified_result.get("queries", [])
exa_config = unified_result.get("exa_config", {})
tavily_config = unified_result.get("tavily_config", {})
trends_config = unified_result.get("trends_config", {}) # NEW: Google Trends config
# Build optimized config with AI-driven justifications
optimized_config = {
"provider": unified_result.get("recommended_provider", "exa"),
"provider_justification": unified_result.get("provider_justification", ""),
# Exa settings with justifications
"exa_type": exa_config.get("type", "auto"),
"exa_type_justification": exa_config.get("type_justification", ""),
"exa_category": exa_config.get("category"),
"exa_category_justification": exa_config.get("category_justification", ""),
"exa_include_domains": exa_config.get("includeDomains", []),
"exa_include_domains_justification": exa_config.get("includeDomains_justification", ""),
"exa_num_results": exa_config.get("numResults", 10),
"exa_num_results_justification": exa_config.get("numResults_justification", ""),
"exa_date_filter": exa_config.get("startPublishedDate"),
"exa_date_justification": exa_config.get("date_justification", ""),
"exa_highlights": exa_config.get("highlights", True),
"exa_highlights_justification": exa_config.get("highlights_justification", ""),
"exa_context": exa_config.get("context", True),
"exa_context_justification": exa_config.get("context_justification", ""),
# Tavily settings with justifications
"tavily_topic": tavily_config.get("topic", "general"),
"tavily_topic_justification": tavily_config.get("topic_justification", ""),
"tavily_search_depth": tavily_config.get("search_depth", "advanced"),
"tavily_search_depth_justification": tavily_config.get("search_depth_justification", ""),
"tavily_include_answer": tavily_config.get("include_answer", True),
"tavily_include_answer_justification": tavily_config.get("include_answer_justification", ""),
"tavily_time_range": tavily_config.get("time_range"),
"tavily_time_range_justification": tavily_config.get("time_range_justification", ""),
"tavily_max_results": tavily_config.get("max_results", 10),
"tavily_max_results_justification": tavily_config.get("max_results_justification", ""),
"tavily_raw_content": tavily_config.get("include_raw_content", "markdown"),
"tavily_raw_content_justification": tavily_config.get("include_raw_content_justification", ""),
}
# Build trends config response (if enabled)
trends_config_response = None
if trends_config.get("enabled", False):
trends_config_response = {
"enabled": True,
"keywords": trends_config.get("keywords", []),
"keywords_justification": trends_config.get("keywords_justification", ""),
"timeframe": trends_config.get("timeframe", "today 12-m"),
"timeframe_justification": trends_config.get("timeframe_justification", ""),
"geo": trends_config.get("geo", "US"),
"geo_justification": trends_config.get("geo_justification", ""),
"expected_insights": trends_config.get("expected_insights", []),
}
return AnalyzeIntentResponse(
success=True,
intent=intent.dict() if hasattr(intent, 'dict') else intent,
analysis_summary=unified_result.get("analysis_summary", ""),
suggested_queries=[q.dict() if hasattr(q, 'dict') else q for q in queries],
suggested_keywords=unified_result.get("enhanced_keywords", []),
suggested_angles=unified_result.get("research_angles", []),
quick_options=[], # Deprecated in unified approach
confidence_reason=intent.confidence_reason if hasattr(intent, 'confidence_reason') else "",
great_example=intent.great_example if hasattr(intent, 'great_example') else "",
optimized_config=optimized_config,
recommended_provider=unified_result.get("recommended_provider", "exa"),
trends_config=trends_config_response, # NEW: Google Trends configuration
)
except Exception as e:
logger.error(f"[Intent API] Analyze failed: {e}")
return AnalyzeIntentResponse(
success=False,
intent={},
analysis_summary="",
suggested_queries=[],
suggested_keywords=[],
suggested_angles=[],
quick_options=[],
confidence_reason=None,
great_example=None,
error_message=str(e),
)
@router.post("/intent/research", response_model=IntentDrivenResearchResponse)
async def execute_intent_driven_research(
request: IntentDrivenResearchRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Execute research based on user intent.
This is the main endpoint for intent-driven research. It:
1. Uses the confirmed intent (or infers from user_input if not provided)
2. Generates targeted queries for each expected deliverable
3. Executes research using Exa/Tavily/Google
4. Analyzes results through the lens of user intent
5. Returns exactly what the user needs
The response is organized by deliverable type (statistics, quotes, case studies, etc.)
instead of generic search results.
"""
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="Invalid user ID")
logger.info(f"[Intent API] Executing intent-driven research for: {request.user_input[:50]}...")
# Get database session
db = next(get_db())
try:
# Get research persona
from services.research.research_persona_service import ResearchPersonaService
persona_service = ResearchPersonaService(db)
research_persona = persona_service.get_or_generate(user_id)
# Determine intent
if request.confirmed_intent:
# Use confirmed intent from UI
intent = ResearchIntent(**request.confirmed_intent)
elif not request.skip_inference:
# Infer intent from user input
intent_service = ResearchIntentInference()
intent_response = await intent_service.infer_intent(
user_input=request.user_input,
research_persona=research_persona,
user_id=user_id,
)
intent = intent_response.intent
else:
# Create basic intent from input
intent = ResearchIntent(
primary_question=f"What are the key insights about: {request.user_input}?",
purpose="learn",
content_output="general",
expected_deliverables=["key_statistics", "best_practices", "examples"],
depth="detailed",
original_input=request.user_input,
confidence=0.6,
)
# Generate or use provided queries
if request.selected_queries:
queries = [ResearchQuery(**q) for q in request.selected_queries]
else:
query_generator = IntentQueryGenerator()
query_result = await query_generator.generate_queries(
intent=intent,
research_persona=research_persona,
user_id=user_id,
)
queries = query_result.get("queries", [])
# Execute research using the Research Engine
engine = ResearchEngine(db_session=db)
# Build context from intent
personalization = ResearchPersonalizationContext(
creator_id=user_id,
industry=research_persona.default_industry if research_persona else None,
target_audience=research_persona.default_target_audience if research_persona else None,
)
# Use the highest priority query for the main search
# (In a more advanced version, we could run multiple queries and merge)
primary_query = queries[0] if queries else ResearchQuery(
query=request.user_input,
purpose=ExpectedDeliverable.KEY_STATISTICS,
provider="exa",
priority=5,
expected_results="General research results",
)
context = ResearchContext(
query=primary_query.query,
keywords=request.user_input.split()[:10],
goal=map_purpose_to_goal(intent.purpose),
depth=map_depth_to_engine_depth(intent.depth),
provider_preference=map_provider_to_preference(primary_query.provider),
personalization=personalization,
max_sources=request.max_sources,
include_domains=request.include_domains,
exclude_domains=request.exclude_domains,
)
# Execute research and trends in parallel
research_task = asyncio.create_task(engine.research(context))
# Execute Google Trends analysis in parallel (if enabled)
trends_task = None
trends_data = None
if request.trends_config and request.trends_config.get("enabled"):
from services.research.trends.google_trends_service import GoogleTrendsService
trends_service = GoogleTrendsService()
trends_task = asyncio.create_task(
trends_service.analyze_trends(
keywords=request.trends_config.get("keywords", []),
timeframe=request.trends_config.get("timeframe", "today 12-m"),
geo=request.trends_config.get("geo", "US"),
user_id=user_id
)
)
# Wait for research to complete
raw_result = await research_task
# Wait for trends if it was started
if trends_task:
try:
trends_data = await trends_task
logger.info(f"Google Trends data fetched: {len(trends_data.get('interest_over_time', []))} time points")
except Exception as e:
logger.error(f"Google Trends analysis failed: {e}")
trends_data = None
# Analyze results using intent-aware analyzer
analyzer = IntentAwareAnalyzer()
analyzed_result = await analyzer.analyze(
raw_results={
"content": raw_result.raw_content or "",
"sources": raw_result.sources,
"grounding_metadata": raw_result.grounding_metadata,
},
intent=intent,
research_persona=research_persona,
user_id=user_id, # Required for subscription checking
)
# Merge Google Trends data into trends analysis
if trends_data and analyzed_result.trends:
analyzed_result = merge_trends_data(analyzed_result, trends_data)
# Build response
return IntentDrivenResearchResponse(
success=True,
primary_answer=analyzed_result.primary_answer,
secondary_answers=analyzed_result.secondary_answers,
focus_areas_coverage=analyzed_result.focus_areas_coverage,
also_answering_coverage=analyzed_result.also_answering_coverage,
statistics=[s.dict() for s in analyzed_result.statistics],
expert_quotes=[q.dict() for q in analyzed_result.expert_quotes],
case_studies=[cs.dict() for cs in analyzed_result.case_studies],
trends=[t.dict() for t in analyzed_result.trends],
comparisons=[c.dict() for c in analyzed_result.comparisons],
best_practices=analyzed_result.best_practices,
step_by_step=analyzed_result.step_by_step,
pros_cons=analyzed_result.pros_cons.dict() if analyzed_result.pros_cons else None,
definitions=analyzed_result.definitions,
examples=analyzed_result.examples,
predictions=analyzed_result.predictions,
executive_summary=analyzed_result.executive_summary,
key_takeaways=analyzed_result.key_takeaways,
suggested_outline=analyzed_result.suggested_outline,
sources=[s.dict() for s in analyzed_result.sources],
confidence=analyzed_result.confidence,
gaps_identified=analyzed_result.gaps_identified,
follow_up_queries=analyzed_result.follow_up_queries,
intent=intent.dict(),
google_trends_data=trends_data, # Include Google Trends data in response
)
finally:
db.close()
except Exception as e:
logger.error(f"[Intent API] Research failed: {e}")
import traceback
traceback.print_exc()
return IntentDrivenResearchResponse(
success=False,
error_message=str(e),
)

View File

@@ -0,0 +1,269 @@
"""
Research Project Handler
CRUD operations for research projects.
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from typing import Optional, Dict, Any
from loguru import logger
import uuid
from sqlalchemy import func
from services.database import get_db
from middleware.auth_middleware import get_current_user
from services.research_service import ResearchService
from models.research_models import ResearchProject
from ..models import (
SaveResearchProjectRequest,
SaveResearchProjectResponse,
ResearchProjectResponse,
ResearchProjectListResponse,
)
router = APIRouter()
@router.post("/projects/save", response_model=SaveResearchProjectResponse)
async def save_research_project(
request: SaveResearchProjectRequest,
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Save a research project to database.
This endpoint saves the complete research project state to the database,
allowing users to resume research later. Similar to podcast projects.
Uses database storage instead of file-based storage for production reliability.
"""
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="Invalid user ID")
logger.info(f"[Research Projects] Saving project: {request.title[:50] if request.title else 'Untitled'}...")
service = ResearchService(db)
# Check if this is an update (project_id provided) or new project
project_id = request.project_id if request.project_id else str(uuid.uuid4())
existing_project = service.get_project(user_id, project_id)
# Determine status based on completion
status = "completed" if (request.intent_result or request.legacy_result) else "in_progress" if request.intent_analysis else "draft"
# Generate title if not provided
project_title = request.title or f"Research: {', '.join(request.keywords[:3])}"
if existing_project:
# Update existing project
updated = service.update_project(
user_id=user_id,
project_id=project_id,
title=project_title,
keywords=request.keywords,
industry=request.industry,
target_audience=request.target_audience,
research_mode=request.research_mode,
config=request.config,
intent_analysis=request.intent_analysis,
confirmed_intent=request.confirmed_intent,
intent_result=request.intent_result,
legacy_result=request.legacy_result,
current_step=request.current_step,
status=status,
)
if updated:
logger.info(f"✅ Research project updated in database: project_id={project_id}, db_id={updated.id}")
return SaveResearchProjectResponse(
success=True,
asset_id=updated.id,
project_id=project_id,
message=f"Research project updated successfully"
)
else:
return SaveResearchProjectResponse(
success=False,
message="Failed to update research project"
)
else:
# Create new project
project = service.create_project(
user_id=user_id,
project_id=project_id,
keywords=request.keywords,
industry=request.industry,
target_audience=request.target_audience,
research_mode=request.research_mode,
title=project_title,
config=request.config,
intent_analysis=request.intent_analysis,
confirmed_intent=request.confirmed_intent,
intent_result=request.intent_result,
legacy_result=request.legacy_result,
current_step=request.current_step,
status=status,
)
logger.info(f"✅ Research project saved to database: project_id={project_id}, db_id={project.id}")
return SaveResearchProjectResponse(
success=True,
asset_id=project.id,
project_id=project_id,
message=f"Research project saved successfully"
)
except Exception as e:
logger.error(f"[Research Projects] Save failed: {e}")
import traceback
traceback.print_exc()
return SaveResearchProjectResponse(
success=False,
message=f"Error saving research project: {str(e)}"
)
@router.get("/projects/{project_id}", response_model=ResearchProjectResponse)
async def get_research_project(
project_id: str,
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Get a research project by ID."""
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="Invalid user ID")
service = ResearchService(db)
project = service.get_project(user_id, project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
return ResearchProjectResponse.model_validate(project)
except HTTPException:
raise
except Exception as e:
logger.error(f"[Research Projects] Get failed: {e}")
raise HTTPException(status_code=500, detail=f"Error fetching project: {str(e)}")
@router.get("/projects", response_model=ResearchProjectListResponse)
async def list_research_projects(
status: Optional[str] = Query(None, description="Filter by status"),
is_favorite: Optional[bool] = Query(None, description="Filter by favorite"),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""List user's research projects."""
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="Invalid user ID")
service = ResearchService(db)
projects = service.list_projects(
user_id=user_id,
status=status,
is_favorite=is_favorite,
limit=limit,
offset=offset,
)
# Get total count
total_query = db.query(func.count(ResearchProject.id)).filter(ResearchProject.user_id == user_id)
if status:
total_query = total_query.filter(ResearchProject.status == status)
if is_favorite is not None:
total_query = total_query.filter(ResearchProject.is_favorite == is_favorite)
total = total_query.scalar()
return ResearchProjectListResponse(
projects=[ResearchProjectResponse.model_validate(p) for p in projects],
total=total,
limit=limit,
offset=offset,
)
except HTTPException:
raise
except Exception as e:
logger.error(f"[Research Projects] List failed: {e}")
raise HTTPException(status_code=500, detail=f"Error listing projects: {str(e)}")
@router.put("/projects/{project_id}", response_model=ResearchProjectResponse)
async def update_research_project(
project_id: str,
updates: Dict[str, Any],
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Update a research project (e.g., toggle favorite, update title)."""
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="Invalid user ID")
service = ResearchService(db)
updated = service.update_project(
user_id=user_id,
project_id=project_id,
**updates
)
if not updated:
raise HTTPException(status_code=404, detail="Project not found")
return ResearchProjectResponse.model_validate(updated)
except HTTPException:
raise
except Exception as e:
logger.error(f"[Research Projects] Update failed: {e}")
raise HTTPException(status_code=500, detail=f"Error updating project: {str(e)}")
@router.delete("/projects/{project_id}", status_code=204)
async def delete_research_project(
project_id: str,
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Delete a research project."""
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="Invalid user ID")
service = ResearchService(db)
deleted = service.delete_project(user_id, project_id)
if not deleted:
raise HTTPException(status_code=404, detail="Project not found")
return None
except HTTPException:
raise
except Exception as e:
logger.error(f"[Research Projects] Delete failed: {e}")
raise HTTPException(status_code=500, detail=f"Error deleting project: {str(e)}")

View File

@@ -0,0 +1,33 @@
"""
Provider Status Handler
Handles provider availability and status endpoints.
"""
from fastapi import APIRouter
from loguru import logger
from services.research.core import ResearchEngine
from ..models import ProviderStatusResponse
router = APIRouter()
@router.get("/providers/status", response_model=ProviderStatusResponse)
async def get_provider_status():
"""
Get status of available research providers.
Returns availability and priority of Exa, Tavily, and Google providers.
"""
try:
engine = ResearchEngine()
return engine.get_provider_status()
except Exception as e:
logger.error(f"[Provider Status] Failed: {e}")
# Return default status on error
return ProviderStatusResponse(
exa={"available": False, "error": str(e)},
tavily={"available": False, "error": str(e)},
google={"available": False, "error": str(e)},
)

View File

@@ -0,0 +1,186 @@
"""
Research Execution Handler
Handles research execution endpoints (execute, start, status, cancel).
"""
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from typing import Dict, Any
from loguru import logger
import uuid
from services.database import get_db
from services.research.core import ResearchEngine, ResearchContext
from middleware.auth_middleware import get_current_user
from ..models import ResearchRequest, ResearchResponse
from ..utils import convert_to_research_context
router = APIRouter()
# In-memory task storage for async research
# TODO: In production, use Redis or database for persistence
_research_tasks: Dict[str, Dict[str, Any]] = {}
@router.post("/execute", response_model=ResearchResponse)
async def execute_research(
request: ResearchRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Execute research synchronously.
For quick research needs. For longer research, use /start endpoint.
"""
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="Invalid user ID in authentication token")
logger.info(f"[Research API] Execute request: {request.query[:50]}...")
engine = ResearchEngine()
context = convert_to_research_context(request, user_id)
result = await engine.research(context)
return ResearchResponse(
success=result.success,
sources=result.sources,
keyword_analysis=result.keyword_analysis,
competitor_analysis=result.competitor_analysis,
suggested_angles=result.suggested_angles,
provider_used=result.provider_used,
search_queries=result.search_queries,
error_message=result.error_message,
error_code=result.error_code,
)
except Exception as e:
logger.error(f"[Research API] Execute failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/start", response_model=ResearchResponse)
async def start_research(
request: ResearchRequest,
background_tasks: BackgroundTasks,
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Start research asynchronously.
Returns a task_id that can be used to poll for status.
Use this for comprehensive research that may take longer.
"""
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="Invalid user ID in authentication token")
logger.info(f"[Research API] Start async request: {request.query[:50]}...")
task_id = str(uuid.uuid4())
# Initialize task
_research_tasks[task_id] = {
"status": "pending",
"progress_messages": [],
"result": None,
"error": None,
}
# Start background task
context = convert_to_research_context(request, user_id)
background_tasks.add_task(_run_research_task, task_id, context)
return ResearchResponse(
success=True,
task_id=task_id,
)
except Exception as e:
logger.error(f"[Research API] Start failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
async def _run_research_task(task_id: str, context: ResearchContext):
"""Background task to run research."""
try:
_research_tasks[task_id]["status"] = "running"
def progress_callback(message: str):
_research_tasks[task_id]["progress_messages"].append(message)
engine = ResearchEngine()
result = await engine.research(context, progress_callback=progress_callback)
_research_tasks[task_id]["status"] = "completed"
_research_tasks[task_id]["result"] = result
except Exception as e:
logger.error(f"[Research API] Task {task_id} failed: {e}")
_research_tasks[task_id]["status"] = "failed"
_research_tasks[task_id]["error"] = str(e)
@router.get("/status/{task_id}")
async def get_research_status(task_id: str):
"""
Get status of an async research task.
Poll this endpoint to get progress updates and final results.
"""
if task_id not in _research_tasks:
raise HTTPException(status_code=404, detail="Task not found")
task = _research_tasks[task_id]
response = {
"task_id": task_id,
"status": task["status"],
"progress_messages": task["progress_messages"],
}
if task["status"] == "completed" and task["result"]:
result = task["result"]
response["result"] = {
"success": result.success,
"sources": result.sources,
"keyword_analysis": result.keyword_analysis,
"competitor_analysis": result.competitor_analysis,
"suggested_angles": result.suggested_angles,
"provider_used": result.provider_used,
"search_queries": result.search_queries,
}
# Clean up completed task after returning
# In production, use Redis or database for persistence
elif task["status"] == "failed":
response["error"] = task["error"]
return response
@router.delete("/status/{task_id}")
async def cancel_research(task_id: str):
"""
Cancel a running research task.
"""
if task_id not in _research_tasks:
raise HTTPException(status_code=404, detail="Task not found")
task = _research_tasks[task_id]
if task["status"] in ["pending", "running"]:
task["status"] = "cancelled"
return {"message": "Task cancelled", "task_id": task_id}
return {"message": f"Task already {task['status']}", "task_id": task_id}