430 lines
17 KiB
Python
430 lines
17 KiB
Python
"""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()
|
|
} |