338 lines
12 KiB
Python
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()
|
|
} |