""" Research API Router Standalone API endpoints for the Research Engine. These endpoints can be used by: - Frontend Research UI - Blog Writer (via adapter) - Podcast Maker - YouTube Creator - Any other content tool Author: ALwrity Team Version: 2.0 """ from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks from pydantic import BaseModel, Field from typing import Optional, List, Dict, Any from loguru import logger import uuid import asyncio from models.research_intent_models import TrendAnalysis from services.database import get_db from services.research.core import ( ResearchEngine, ResearchContext, ResearchPersonalizationContext, ContentType, ResearchGoal, ResearchDepth, ProviderPreference, ) from services.research.core.research_context import ResearchResult from middleware.auth_middleware import get_current_user # Intent-driven research imports from models.research_intent_models import ( ResearchIntent, IntentInferenceRequest, IntentInferenceResponse, IntentDrivenResearchResult, ResearchQuery, ExpectedDeliverable, ResearchPurpose, ContentOutput, ResearchDepthLevel, ) from services.research.intent import ( ResearchIntentInference, IntentQueryGenerator, IntentAwareAnalyzer, ) router = APIRouter(prefix="/api/research", tags=["Research Engine"]) # Request/Response models class ResearchRequest(BaseModel): """API request for research.""" query: str = Field(..., description="Main research query or topic") keywords: List[str] = Field(default_factory=list, description="Additional keywords") # Research configuration goal: Optional[str] = Field(default="factual", description="Research goal: factual, trending, competitive, etc.") depth: Optional[str] = Field(default="standard", description="Research depth: quick, standard, comprehensive, expert") provider: Optional[str] = Field(default="auto", description="Provider preference: auto, exa, tavily, google") # Personalization content_type: Optional[str] = Field(default="general", description="Content type: blog, podcast, video, etc.") industry: Optional[str] = None target_audience: Optional[str] = None tone: Optional[str] = None # Constraints max_sources: int = Field(default=10, ge=1, le=25) recency: Optional[str] = None # day, week, month, year # Domain filtering include_domains: List[str] = Field(default_factory=list) exclude_domains: List[str] = Field(default_factory=list) # Advanced mode advanced_mode: bool = False # Raw provider parameters (only if advanced_mode=True) exa_category: Optional[str] = None exa_search_type: Optional[str] = None tavily_topic: Optional[str] = None tavily_search_depth: Optional[str] = None tavily_include_answer: bool = False tavily_time_range: Optional[str] = None class ResearchResponse(BaseModel): """API response for research.""" success: bool task_id: Optional[str] = None # For async requests # Results (if synchronous) sources: List[Dict[str, Any]] = Field(default_factory=list) keyword_analysis: Dict[str, Any] = Field(default_factory=dict) competitor_analysis: Dict[str, Any] = Field(default_factory=dict) suggested_angles: List[str] = Field(default_factory=list) # Metadata provider_used: Optional[str] = None search_queries: List[str] = Field(default_factory=list) # Error handling error_message: Optional[str] = None error_code: Optional[str] = None class ProviderStatusResponse(BaseModel): """API response for provider status.""" exa: Dict[str, Any] tavily: Dict[str, Any] google: Dict[str, Any] # In-memory task storage for async research _research_tasks: Dict[str, Dict[str, Any]] = {} def _convert_to_research_context(request: ResearchRequest, user_id: str) -> ResearchContext: """Convert API request to ResearchContext.""" # Map string enums goal_map = { "factual": ResearchGoal.FACTUAL, "trending": ResearchGoal.TRENDING, "competitive": ResearchGoal.COMPETITIVE, "educational": ResearchGoal.EDUCATIONAL, "technical": ResearchGoal.TECHNICAL, "inspirational": ResearchGoal.INSPIRATIONAL, } depth_map = { "quick": ResearchDepth.QUICK, "standard": ResearchDepth.STANDARD, "comprehensive": ResearchDepth.COMPREHENSIVE, "expert": ResearchDepth.EXPERT, } provider_map = { "auto": ProviderPreference.AUTO, "exa": ProviderPreference.EXA, "tavily": ProviderPreference.TAVILY, "google": ProviderPreference.GOOGLE, "hybrid": ProviderPreference.HYBRID, } content_type_map = { "blog": ContentType.BLOG, "podcast": ContentType.PODCAST, "video": ContentType.VIDEO, "social": ContentType.SOCIAL, "email": ContentType.EMAIL, "newsletter": ContentType.NEWSLETTER, "whitepaper": ContentType.WHITEPAPER, "general": ContentType.GENERAL, } # Build personalization context personalization = ResearchPersonalizationContext( creator_id=user_id, content_type=content_type_map.get(request.content_type or "general", ContentType.GENERAL), industry=request.industry, target_audience=request.target_audience, tone=request.tone, ) return ResearchContext( query=request.query, keywords=request.keywords, goal=goal_map.get(request.goal or "factual", ResearchGoal.FACTUAL), depth=depth_map.get(request.depth or "standard", ResearchDepth.STANDARD), provider_preference=provider_map.get(request.provider or "auto", ProviderPreference.AUTO), personalization=personalization, max_sources=request.max_sources, recency=request.recency, include_domains=request.include_domains, exclude_domains=request.exclude_domains, advanced_mode=request.advanced_mode, exa_category=request.exa_category, exa_search_type=request.exa_search_type, tavily_topic=request.tavily_topic, tavily_search_depth=request.tavily_search_depth, tavily_include_answer=request.tavily_include_answer, tavily_time_range=request.tavily_time_range, ) @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. """ engine = ResearchEngine() return engine.get_provider_status() @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} # ============================================================================ # Intent-Driven Research Endpoints # ============================================================================ class AnalyzeIntentRequest(BaseModel): """Request to analyze user research intent.""" user_input: str = Field(..., description="User's keywords, question, or goal") keywords: List[str] = Field(default_factory=list, description="Extracted keywords") use_persona: bool = Field(True, description="Use research persona for context") use_competitor_data: bool = Field(True, description="Use competitor data for context") class AnalyzeIntentResponse(BaseModel): """Response from intent analysis with optimized provider parameters.""" success: bool intent: Dict[str, Any] analysis_summary: str suggested_queries: List[Dict[str, Any]] suggested_keywords: List[str] suggested_angles: List[str] quick_options: List[Dict[str, Any]] confidence_reason: Optional[str] = None great_example: Optional[str] = None error_message: Optional[str] = None # Unified: Optimized provider parameters based on intent optimized_config: Optional[Dict[str, Any]] = None # Provider settings auto-configured from intent recommended_provider: Optional[str] = None # Best provider for this intent (exa, tavily, google) # Google Trends configuration (if trends in deliverables) trends_config: Optional[Dict[str, Any]] = None # Trends keywords and settings with justifications class IntentDrivenResearchRequest(BaseModel): """Request for intent-driven research.""" # Intent from previous analyze step, or minimal input for auto-inference user_input: str = Field(..., description="User's original input") # Optional: Confirmed intent from UI (if user modified the inferred intent) confirmed_intent: Optional[Dict[str, Any]] = None # Optional: Specific queries to run (if user selected from suggested) selected_queries: Optional[List[Dict[str, Any]]] = None # Research configuration max_sources: int = Field(default=10, ge=1, le=25) include_domains: List[str] = Field(default_factory=list) exclude_domains: List[str] = Field(default_factory=list) # Google Trends configuration (from intent analysis) trends_config: Optional[Dict[str, Any]] = None # Trends keywords and settings # Skip intent inference (for re-runs with same intent) skip_inference: bool = False class IntentDrivenResearchResponse(BaseModel): """Response from intent-driven research.""" success: bool # Direct answers primary_answer: str = "" secondary_answers: Dict[str, str] = Field(default_factory=dict) # Deliverables statistics: List[Dict[str, Any]] = Field(default_factory=list) expert_quotes: List[Dict[str, Any]] = Field(default_factory=list) case_studies: List[Dict[str, Any]] = Field(default_factory=list) trends: List[Dict[str, Any]] = Field(default_factory=list) comparisons: List[Dict[str, Any]] = Field(default_factory=list) best_practices: List[str] = Field(default_factory=list) step_by_step: List[str] = Field(default_factory=list) pros_cons: Optional[Dict[str, Any]] = None definitions: Dict[str, str] = Field(default_factory=dict) examples: List[str] = Field(default_factory=list) predictions: List[str] = Field(default_factory=list) # Content-ready outputs executive_summary: str = "" key_takeaways: List[str] = Field(default_factory=list) suggested_outline: List[str] = Field(default_factory=list) # Sources and metadata sources: List[Dict[str, Any]] = Field(default_factory=list) confidence: float = 0.8 gaps_identified: List[str] = Field(default_factory=list) follow_up_queries: List[str] = Field(default_factory=list) # The inferred/confirmed intent intent: Optional[Dict[str, Any]] = None # Google Trends data (if trends were analyzed) google_trends_data: Optional[Dict[str, Any]] = None # Error handling error_message: Optional[str] = None @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, ) 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, 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), ) def _map_purpose_to_goal(purpose: str) -> ResearchGoal: """Map intent purpose to research goal.""" mapping = { "learn": ResearchGoal.EDUCATIONAL, "create_content": ResearchGoal.FACTUAL, "make_decision": ResearchGoal.FACTUAL, "compare": ResearchGoal.COMPETITIVE, "solve_problem": ResearchGoal.EDUCATIONAL, "find_data": ResearchGoal.FACTUAL, "explore_trends": ResearchGoal.TRENDING, "validate": ResearchGoal.FACTUAL, "generate_ideas": ResearchGoal.INSPIRATIONAL, } return mapping.get(purpose, ResearchGoal.FACTUAL) def _map_depth_to_engine_depth(depth: str) -> ResearchDepth: """Map intent depth to research engine depth.""" mapping = { "overview": ResearchDepth.QUICK, "detailed": ResearchDepth.STANDARD, "expert": ResearchDepth.COMPREHENSIVE, } return mapping.get(depth, ResearchDepth.STANDARD) def _map_provider_to_preference(provider: str) -> ProviderPreference: """Map query provider to engine preference.""" mapping = { "exa": ProviderPreference.EXA, "tavily": ProviderPreference.TAVILY, "google": ProviderPreference.GOOGLE, } return mapping.get(provider, ProviderPreference.AUTO) def _merge_trends_data( analyzed_result: Any, trends_data: Dict[str, Any] ) -> Any: """ Merge Google Trends data into analyzed result trends. Enhances AI-extracted trends with Google Trends data. """ from services.research.intent.intent_aware_analyzer import IntentDrivenResearchResult from models.research_intent_models import TrendAnalysis if not analyzed_result.trends: return analyzed_result # Enhance each trend with Google Trends data enhanced_trends = [] for trend in analyzed_result.trends: # Create enhanced trend with Google Trends data trend_dict = trend.dict() if hasattr(trend, 'dict') else trend trend_dict["google_trends_data"] = trends_data # Add interest score if available if trends_data.get("interest_over_time"): # Calculate average interest score interest_values = [] for point in trends_data["interest_over_time"]: for key, value in point.items(): if key not in ["date", "isPartial"] and isinstance(value, (int, float)): interest_values.append(value) if interest_values: trend_dict["interest_score"] = sum(interest_values) / len(interest_values) # Add related topics/queries if trends_data.get("related_topics"): top_topics = [t.get("topic_title", "") for t in trends_data["related_topics"].get("top", [])[:5]] rising_topics = [t.get("topic_title", "") for t in trends_data["related_topics"].get("rising", [])[:5]] trend_dict["related_topics"] = {"top": top_topics, "rising": rising_topics} if trends_data.get("related_queries"): top_queries = [q.get("query", "") for q in trends_data["related_queries"].get("top", [])[:5]] rising_queries = [q.get("query", "") for q in trends_data["related_queries"].get("rising", [])[:5]] trend_dict["related_queries"] = {"top": top_queries, "rising": rising_queries} # Add regional interest if trends_data.get("interest_by_region"): regional_interest = {} for region in trends_data["interest_by_region"][:10]: # Top 10 regions region_name = region.get("geoName", "") if region_name: # Get interest value (first numeric column) for key, value in region.items(): if key != "geoName" and isinstance(value, (int, float)): regional_interest[region_name] = value break trend_dict["regional_interest"] = regional_interest enhanced_trends.append(TrendAnalysis(**trend_dict)) # Update analyzed result with enhanced trends analyzed_result.trends = enhanced_trends return analyzed_result