youtube-summarizer/backend/api/multi_agent.py

338 lines
12 KiB
Python

"""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()
}