"""Multi-agent analysis API endpoints.""" import logging import asyncio from typing import Dict, List, Optional, Any from datetime import datetime import uuid from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks from fastapi.responses import JSONResponse from pydantic import BaseModel, Field from sqlalchemy.orm import Session from backend.core.database import get_db from backend.core.exceptions import ServiceError from backend.services.multi_agent_orchestrator import MultiAgentVideoOrchestrator from backend.services.playlist_analyzer import PlaylistAnalyzer from backend.services.transcript_service import TranscriptService from backend.services.video_service import VideoService from backend.services.playlist_service import PlaylistService # Removed - will create local dependency functions logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/analysis", tags=["multi-agent"]) # Dependency injection functions def get_transcript_service() -> TranscriptService: """Get transcript service instance.""" return TranscriptService() def get_video_service() -> VideoService: """Get video service instance.""" return VideoService() # Request/Response Models class MultiAgentAnalysisRequest(BaseModel): """Request for multi-agent analysis of a single video.""" agent_types: Optional[List[str]] = Field( default=["technical", "business", "user_experience"], description="Agent perspectives to include" ) include_synthesis: bool = Field(default=True, description="Include synthesis agent") class PerspectiveAnalysisResponse(BaseModel): """Response model for individual perspective analysis.""" agent_type: str summary: str key_insights: List[str] confidence_score: float focus_areas: List[str] recommendations: List[str] processing_time_seconds: float agent_id: str class MultiAgentAnalysisResponse(BaseModel): """Response model for complete multi-agent analysis.""" video_id: str video_title: str perspectives: Dict[str, PerspectiveAnalysisResponse] unified_insights: List[str] processing_time_seconds: float quality_score: float created_at: str class PlaylistAnalysisRequest(BaseModel): """Request for playlist analysis with multi-agent system.""" playlist_url: str = Field(..., description="YouTube playlist URL") include_cross_video_analysis: bool = Field( default=True, description="Include cross-video theme analysis" ) agent_types: List[str] = Field( default=["technical", "business", "user"], description="Agent perspectives for each video" ) max_videos: Optional[int] = Field( default=20, description="Maximum number of videos to process" ) class PlaylistAnalysisJobResponse(BaseModel): """Response for playlist analysis job creation.""" job_id: str status: str playlist_url: str estimated_videos: Optional[int] = None estimated_completion_time: Optional[str] = None class PlaylistAnalysisStatusResponse(BaseModel): """Response for playlist analysis job status.""" job_id: str status: str progress_percentage: float current_video: Optional[str] = None videos_completed: int videos_total: int results: Optional[Dict[str, Any]] = None error: Optional[str] = None # Playlist processing now handled by PlaylistService # Dependencies def get_multi_agent_orchestrator() -> MultiAgentVideoOrchestrator: """Get multi-agent orchestrator instance.""" return MultiAgentVideoOrchestrator() def get_playlist_analyzer() -> PlaylistAnalyzer: """Get playlist analyzer instance.""" return PlaylistAnalyzer() def get_playlist_service() -> PlaylistService: """Get playlist service instance.""" return PlaylistService() @router.post( "/multi-agent/{video_id}", response_model=MultiAgentAnalysisResponse, summary="Analyze video with multiple agent perspectives" ) async def analyze_video_multi_agent( video_id: str, request: MultiAgentAnalysisRequest, orchestrator: MultiAgentVideoOrchestrator = Depends(get_multi_agent_orchestrator), transcript_service: TranscriptService = Depends(get_transcript_service), video_service: VideoService = Depends(get_video_service), db: Session = Depends(get_db) ): """ Analyze a single video using multiple AI agent perspectives. Returns analysis from Technical, Business, and User Experience agents, plus an optional synthesis combining all perspectives. """ try: logger.info(f"Starting multi-agent analysis for video: {video_id}") # Validate agent types valid_agents = {"technical", "business", "user", "synthesis"} invalid_agents = set(request.agent_types) - valid_agents if invalid_agents: raise HTTPException( status_code=400, detail=f"Invalid agent types: {invalid_agents}" ) # Get video metadata try: video_metadata = await video_service.get_video_info(video_id) video_title = video_metadata.get('title', '') except Exception as e: logger.warning(f"Could not get video metadata for {video_id}: {e}") video_title = "" # Get transcript try: transcript_result = await transcript_service.extract_transcript(video_id) if not transcript_result or not transcript_result.get('transcript'): raise HTTPException( status_code=400, detail="Could not extract transcript for video" ) transcript = transcript_result['transcript'] except Exception as e: logger.error(f"Transcript extraction failed for {video_id}: {e}") raise HTTPException( status_code=400, detail=f"Transcript extraction failed: {str(e)}" ) # Perform multi-agent analysis using the orchestrator analysis_result = await orchestrator.analyze_video_with_multiple_perspectives( transcript=transcript, video_id=video_id, video_title=video_title, perspectives=request.agent_types ) logger.info(f"Multi-agent analysis completed for video: {video_id}") return analysis_result except HTTPException: raise except ServiceError as e: logger.error(f"Service error in multi-agent analysis: {e}") raise HTTPException(status_code=500, detail=str(e)) except Exception as e: logger.error(f"Unexpected error in multi-agent analysis: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get( "/agent-perspectives/{summary_id}", summary="Get all agent perspectives for a summary" ) async def get_agent_perspectives( summary_id: str, db: Session = Depends(get_db) ): """ Retrieve all agent perspectives for a previously analyzed video. This endpoint would typically query the agent_summaries table to return stored multi-agent analyses. """ # TODO: Implement database query for agent_summaries # For now, return placeholder response return { "summary_id": summary_id, "message": "Agent perspectives retrieval not yet implemented", "note": "This would query the agent_summaries database table" } @router.post( "/playlist", response_model=PlaylistAnalysisJobResponse, summary="Start playlist analysis with multi-agent system" ) async def analyze_playlist( request: PlaylistAnalysisRequest, playlist_service: PlaylistService = Depends(get_playlist_service) ): """ Start multi-agent analysis of an entire YouTube playlist. Processes each video in the playlist with the specified agent perspectives and performs cross-video analysis to identify themes and patterns. """ try: logger.info(f"Starting playlist analysis for: {request.playlist_url}") # Start playlist processing job_id = await playlist_service.start_playlist_processing( playlist_url=request.playlist_url, max_videos=request.max_videos, agent_types=request.agent_types ) # Get initial job status for response job_status = playlist_service.get_playlist_status(job_id) estimated_videos = request.max_videos or 20 return PlaylistAnalysisJobResponse( job_id=job_id, status="pending", playlist_url=request.playlist_url, estimated_videos=estimated_videos, estimated_completion_time=f"~{estimated_videos * 2} minutes" ) except HTTPException: raise except Exception as e: logger.error(f"Error starting playlist analysis: {e}") raise HTTPException(status_code=500, detail="Failed to start playlist analysis") @router.get( "/playlist/{job_id}/status", response_model=PlaylistAnalysisStatusResponse, summary="Get playlist analysis job status" ) async def get_playlist_status(job_id: str, playlist_service: PlaylistService = Depends(get_playlist_service)): """ Get the current status and progress of a playlist analysis job. Returns real-time progress updates and results as they become available. """ job = playlist_service.get_playlist_status(job_id) if not job: raise HTTPException(status_code=404, detail="Job not found") # Prepare results if completed results = None if job.status == "completed" and job.cross_video_analysis: results = { "playlist_metadata": job.playlist_metadata.__dict__ if job.playlist_metadata else None, "cross_video_analysis": job.cross_video_analysis, "video_analyses": [ { "video_id": v.video_id, "title": v.title, "analysis": v.analysis_result, "error": v.error } for v in job.videos ] } return PlaylistAnalysisStatusResponse( job_id=job_id, status=job.status, progress_percentage=job.progress_percentage, current_video=job.current_video, videos_completed=job.processed_videos, videos_total=len(job.videos), results=results, error=job.error ) @router.delete( "/playlist/{job_id}", summary="Cancel playlist analysis job" ) async def cancel_playlist_analysis(job_id: str, playlist_service: PlaylistService = Depends(get_playlist_service)): """Cancel a running playlist analysis job.""" job = playlist_service.get_playlist_status(job_id) if not job: raise HTTPException(status_code=404, detail="Job not found") if job.status in ["completed", "failed", "cancelled"]: return {"message": f"Job already {job.status}"} # Cancel the job success = playlist_service.cancel_playlist_processing(job_id) if success: return {"message": "Job cancelled successfully"} else: return {"message": "Job could not be cancelled"} # Helper functions (kept for backward compatibility if needed) # Most playlist processing logic is now handled by PlaylistService @router.get( "/health", summary="Multi-agent service health check" ) async def multi_agent_health(): """Check health status of multi-agent analysis service.""" try: orchestrator = MultiAgentVideoOrchestrator() health = await orchestrator.get_orchestrator_health() return health except Exception as e: logger.error(f"Health check failed: {e}") return { "service": "multi_agent_analysis", "status": "error", "error": str(e), "timestamp": datetime.now().isoformat() }