"""Multi-agent orchestration service for YouTube video analysis.""" import asyncio import logging import uuid from typing import Dict, List, Optional, Any from datetime import datetime from sqlalchemy.orm import Session from ..core.exceptions import ServiceError from .deepseek_service import DeepSeekService from .perspective_agents import ( TechnicalAnalysisAgent, BusinessAnalysisAgent, UserExperienceAgent, SynthesisAgent ) from backend.models.agent_models import AgentSummary logger = logging.getLogger(__name__) class MultiAgentVideoOrchestrator: """Orchestrator for multi-agent YouTube video analysis.""" def __init__(self, ai_service: Optional[DeepSeekService] = None): """Initialize the multi-agent orchestrator. Args: ai_service: DeepSeek AI service instance """ self.ai_service = ai_service or DeepSeekService() # Initialize perspective agents self.technical_agent = TechnicalAnalysisAgent(self.ai_service) self.business_agent = BusinessAnalysisAgent(self.ai_service) self.ux_agent = UserExperienceAgent(self.ai_service) self.synthesis_agent = SynthesisAgent(self.ai_service) self._is_initialized = False async def initialize(self) -> None: """Initialize the orchestrator and agents.""" if self._is_initialized: logger.warning("Multi-agent orchestrator already initialized") return logger.info("Initializing multi-agent video orchestrator") try: # Basic initialization - agents are already created self._is_initialized = True logger.info("Multi-agent video orchestrator initialized with 4 perspective agents") except Exception as e: logger.error(f"Failed to initialize multi-agent orchestrator: {e}") raise ServiceError(f"Orchestrator initialization failed: {str(e)}") async def shutdown(self) -> None: """Shutdown the orchestrator gracefully.""" logger.info("Shutting down multi-agent video orchestrator") self._is_initialized = False logger.info("Multi-agent video orchestrator shutdown complete") async def analyze_video_with_multiple_perspectives( self, transcript: str, video_id: str, video_title: str = "", perspectives: Optional[List[str]] = None, thread_id: Optional[str] = None ) -> Dict[str, Any]: """Analyze video content using multiple agent perspectives. Args: transcript: Video transcript text video_id: YouTube video ID video_title: Video title for context perspectives: List of perspectives to analyze (defaults to all) thread_id: Thread ID for continuity (unused in simplified version) Returns: Complete multi-agent analysis result """ if not self._is_initialized: await self.initialize() if not transcript or len(transcript.strip()) < 50: raise ServiceError("Transcript too short for multi-agent analysis") # Default to all perspectives if perspectives is None: perspectives = ["technical", "business", "user_experience"] logger.info(f"Starting multi-agent analysis for video {video_id} with perspectives: {perspectives}") try: # Create analysis state state = { "transcript": transcript, "video_id": video_id, "video_title": video_title, "metadata": { "video_analysis": True, "perspectives": perspectives, } } # Execute perspective analyses in parallel analysis_tasks = [] for perspective in perspectives: if perspective == "technical": task = self._execute_perspective_analysis( agent=self.technical_agent, state=state ) elif perspective == "business": task = self._execute_perspective_analysis( agent=self.business_agent, state=state ) elif perspective == "user_experience": task = self._execute_perspective_analysis( agent=self.ux_agent, state=state ) if task: analysis_tasks.append(task) # Wait for all perspective analyses to complete perspective_results = await asyncio.gather(*analysis_tasks, return_exceptions=True) # Process results and handle exceptions successful_analyses = {} total_processing_time = 0.0 for i, result in enumerate(perspective_results): perspective = perspectives[i] if isinstance(result, Exception): logger.error(f"Error in {perspective} analysis: {result}") continue if result and result.get("status") != "error": analysis_data = result.get("analysis_results", {}) for analysis_key, analysis_content in analysis_data.items(): successful_analyses[analysis_key] = analysis_content total_processing_time += analysis_content.get("processing_time_seconds", 0) if not successful_analyses: raise ServiceError("All perspective analyses failed") # Run synthesis if we have multiple perspectives if len(successful_analyses) > 1: synthesis_state = state.copy() synthesis_state["analysis_results"] = successful_analyses synthesis_result = await self._execute_synthesis( agent=self.synthesis_agent, state=synthesis_state ) if synthesis_result and synthesis_result.get("status") != "error": synthesis_data = synthesis_result.get("analysis_results", {}).get("synthesis") if synthesis_data: successful_analyses["synthesis"] = synthesis_data total_processing_time += synthesis_data.get("processing_time_seconds", 0) # Calculate overall quality score quality_score = self._calculate_quality_score(successful_analyses) # Extract unified insights unified_insights = self._extract_unified_insights(successful_analyses) # Build final result result = { "video_id": video_id, "video_title": video_title, "perspectives": successful_analyses, "unified_insights": unified_insights, "processing_time_seconds": total_processing_time, "quality_score": quality_score, "created_at": datetime.now().isoformat(), "orchestrator_stats": { "agent_count": len(successful_analyses), "perspectives_analyzed": list(successful_analyses.keys()), "total_processing_time": total_processing_time } } logger.info(f"Multi-agent analysis completed for video {video_id} in {total_processing_time:.2f}s") return result except Exception as e: logger.error(f"Error in multi-agent video analysis for {video_id}: {e}") raise ServiceError(f"Multi-agent analysis failed: {str(e)}") async def save_analysis_to_database( self, summary_id: str, analysis_result: Dict[str, Any], db: Session ) -> List[AgentSummary]: """Save multi-agent analysis results to database. Args: summary_id: ID of the summary this analysis belongs to analysis_result: Complete analysis result from analyze_video_with_multiple_perspectives db: Database session Returns: List of AgentSummary objects that were saved """ agent_summaries = [] try: perspectives = analysis_result.get('perspectives', {}) for perspective_type, analysis_data in perspectives.items(): agent_summary = AgentSummary( summary_id=summary_id, agent_type=perspective_type, agent_summary=analysis_data.get('summary'), key_insights=analysis_data.get('key_insights', []), focus_areas=analysis_data.get('focus_areas', []), recommendations=analysis_data.get('recommendations', []), confidence_score=analysis_data.get('confidence_score'), processing_time_seconds=analysis_data.get('processing_time_seconds') ) db.add(agent_summary) agent_summaries.append(agent_summary) db.commit() logger.info(f"Saved {len(agent_summaries)} agent analyses to database for summary {summary_id}") return agent_summaries except Exception as e: db.rollback() logger.error(f"Failed to save agent analyses to database: {e}") raise ServiceError(f"Database save failed: {str(e)}") async def _execute_perspective_analysis( self, agent, state: Dict[str, Any] ) -> Dict[str, Any]: """Execute analysis for a specific perspective agent. Args: agent: The perspective agent to execute state: Analysis state with transcript and metadata Returns: Analysis result from the agent """ try: # Execute the agent directly result_state = await agent.execute(state) return result_state except Exception as e: logger.error(f"Error executing {agent.agent_id}: {e}") return { "status": "error", "error": str(e), "agent_id": agent.agent_id } async def _execute_synthesis( self, agent, state: Dict[str, Any] ) -> Dict[str, Any]: """Execute synthesis of multiple perspective analyses. Args: agent: The synthesis agent state: State with analysis results Returns: Synthesis result """ try: # Execute synthesis agent result_state = await agent.execute(state) return result_state except Exception as e: logger.error(f"Error in synthesis execution: {e}") return { "status": "error", "error": str(e), "agent_id": agent.agent_id } def _calculate_quality_score(self, analyses: Dict[str, Any]) -> float: """Calculate overall quality score from perspective analyses. Args: analyses: Dictionary of perspective analyses Returns: Quality score between 0.0 and 1.0 """ if not analyses: return 0.0 # Average confidence scores confidence_scores = [] completeness_scores = [] for analysis in analyses.values(): if analysis.get("agent_type") == "synthesis": # Synthesis has different structure confidence_scores.append(analysis.get("confidence_score", 0.7)) # Synthesis completeness based on unified insights and recommendations insight_score = min(len(analysis.get("unified_insights", [])) / 8.0, 1.0) rec_score = min(len(analysis.get("recommendations", [])) / 5.0, 1.0) completeness_scores.append((insight_score + rec_score) / 2.0) else: # Regular perspective analysis confidence_scores.append(analysis.get("confidence_score", 0.7)) # Completeness based on insights and recommendations insight_score = min(len(analysis.get("key_insights", [])) / 5.0, 1.0) rec_score = min(len(analysis.get("recommendations", [])) / 3.0, 1.0) completeness_scores.append((insight_score + rec_score) / 2.0) # Calculate averages avg_confidence = sum(confidence_scores) / len(confidence_scores) if confidence_scores else 0.0 avg_completeness = sum(completeness_scores) / len(completeness_scores) if completeness_scores else 0.0 # Weighted final score (confidence weighted more heavily) quality_score = (avg_confidence * 0.7) + (avg_completeness * 0.3) return round(quality_score, 2) def _extract_unified_insights(self, analyses: Dict[str, Any]) -> List[str]: """Extract unified insights from all analyses. Args: analyses: Dictionary of perspective analyses Returns: List of unified insights """ unified_insights = [] # Check if synthesis exists and use its unified insights if "synthesis" in analyses: synthesis_insights = analyses["synthesis"].get("unified_insights", []) unified_insights.extend(synthesis_insights[:8]) # Top 8 from synthesis # Add top insights from each perspective (if no synthesis or to supplement) for perspective_type, analysis in analyses.items(): if perspective_type == "synthesis": continue perspective_insights = analysis.get("key_insights", []) for insight in perspective_insights[:2]: # Top 2 from each perspective if insight and len(unified_insights) < 12: formatted_insight = f"[{perspective_type.title()}] {insight}" if formatted_insight not in unified_insights: unified_insights.append(formatted_insight) return unified_insights[:12] # Limit to 12 total insights async def get_orchestrator_health(self) -> Dict[str, Any]: """Get health status of the multi-agent orchestrator. Returns: Health information for the orchestrator and all agents """ health_info = { "service": "multi_agent_video_orchestrator", "initialized": self._is_initialized, "timestamp": datetime.now().isoformat(), "ai_service_available": self.ai_service is not None } if self._is_initialized: # Get agent information agents = [ {"agent_id": self.technical_agent.agent_id, "name": self.technical_agent.name}, {"agent_id": self.business_agent.agent_id, "name": self.business_agent.name}, {"agent_id": self.ux_agent.agent_id, "name": self.ux_agent.name}, {"agent_id": self.synthesis_agent.agent_id, "name": self.synthesis_agent.name} ] health_info["agents"] = agents health_info["agent_count"] = len(agents) health_info["status"] = "healthy" else: health_info["status"] = "not_initialized" # Test AI service connectivity if self.ai_service: try: await self.ai_service.generate_response("test", max_tokens=10) health_info["ai_service_status"] = "connected" except Exception: health_info["ai_service_status"] = "connection_error" if health_info["status"] == "healthy": health_info["status"] = "degraded" else: health_info["ai_service_status"] = "not_configured" health_info["status"] = "error" return health_info def get_supported_perspectives(self) -> List[str]: """Get list of supported analysis perspectives. Returns: List of perspective names """ return ["technical", "business", "user_experience"] def get_agent_capabilities(self) -> Dict[str, List[str]]: """Get capabilities of each registered agent. Returns: Dictionary mapping agent IDs to their capabilities """ return { "technical_analyst": self.technical_agent.get_capabilities(), "business_analyst": self.business_agent.get_capabilities(), "ux_analyst": self.ux_agent.get_capabilities(), "synthesis_agent": self.synthesis_agent.get_capabilities() }