""" Real-time Interview Monitoring API endpoints Provides endpoints for monitoring active interviews, retrieving transcripts, and emotion events """ from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy.orm import Session from sqlalchemy import and_, desc from datetime import datetime from pydantic import BaseModel from models import get_db, User, Interview from models.transcript_segment import TranscriptSegment from models.emotion_event import EmotionEvent from models.candidate import Candidate from api.auth import get_current_active_user, require_recruiter router = APIRouter(tags=["Monitoring"]) # ============================================================================ # Pydantic Schemas # ============================================================================ class ActiveInterviewSummary(BaseModel): """Summary information for an active interview""" interview_id: int candidate_id: int candidate_name: str job_id: int start_time: datetime current_question_number: Optional[int] latest_emotion: Optional[str] websocket_channel_id: Optional[str] class Config: from_attributes = True class InterviewDetailResponse(BaseModel): """Detailed information for an active interview""" interview_id: int candidate_id: int candidate_name: str job_id: int start_time: datetime status: str is_active: bool websocket_channel_id: Optional[str] overall_score: Optional[float] technical_score: Optional[float] communication_score: Optional[float] questions_asked: Optional[List] class Config: from_attributes = True class TranscriptSegmentResponse(BaseModel): """Response schema for transcript segment""" id: int interview_id: int timestamp: float speaker: str text: str confidence: Optional[float] created_at: datetime class Config: from_attributes = True class EmotionEventResponse(BaseModel): """Response schema for emotion event""" id: int interview_id: int timestamp: float emotion_type: str confidence_score: float audio_confidence: Optional[float] video_confidence: Optional[float] created_at: datetime class Config: from_attributes = True # ============================================================================ # Monitoring Endpoints # ============================================================================ @router.get( "/monitoring/active-interviews", response_model=List[ActiveInterviewSummary], summary="Get all active interviews", description="Retrieve a list of all currently active interviews with summary information" ) async def get_active_interviews( db: Session = Depends(get_db), current_user: User = Depends(require_recruiter) ): """ Get list of all active interviews. **Requirements:** 4.1, 4.2 **Returns:** List of active interview summaries including: - interview_id: ID of the interview - candidate_name: Name of the candidate - start_time: When the interview started - current_question_number: Current question being asked - latest_emotion: Most recent detected emotion **Authorization:** Requires recruiter role or higher """ try: # Query for active interviews active_interviews = db.query(Interview).filter( Interview.is_active == True ).all() summaries = [] for interview in active_interviews: # Get candidate information candidate = db.query(Candidate).filter( Candidate.id == interview.candidate_id ).first() candidate_name = f"{candidate.first_name} {candidate.last_name}" if candidate else "Unknown" # Get current question number from questions_asked current_question_number = None if interview.questions_asked: current_question_number = len(interview.questions_asked) # Get latest emotion latest_emotion = None latest_emotion_event = db.query(EmotionEvent).filter( EmotionEvent.interview_id == interview.id ).order_by(desc(EmotionEvent.timestamp)).first() if latest_emotion_event: latest_emotion = latest_emotion_event.emotion_type summaries.append(ActiveInterviewSummary( interview_id=interview.id, candidate_id=interview.candidate_id, candidate_name=candidate_name, job_id=interview.job_id, start_time=interview.started_at or interview.scheduled_at, current_question_number=current_question_number, latest_emotion=latest_emotion, websocket_channel_id=interview.websocket_channel_id )) return summaries except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error retrieving active interviews: {str(e)}" ) @router.get( "/monitoring/interview/{interview_id}/detail", response_model=InterviewDetailResponse, summary="Get detailed interview information", description="Retrieve detailed information for a specific interview" ) async def get_interview_detail( interview_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_recruiter) ): """ Get detailed information for a specific interview. **Requirements:** 4.5, 5.1 **Parameters:** - interview_id: ID of the interview **Returns:** Detailed interview information including: - Basic interview details - Candidate information - Current scores - Questions asked - Interview status **Authorization:** Requires recruiter role or higher """ try: # Get interview interview = db.query(Interview).filter( Interview.id == interview_id ).first() if not interview: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Interview with id {interview_id} not found" ) # Get candidate information candidate = db.query(Candidate).filter( Candidate.id == interview.candidate_id ).first() candidate_name = f"{candidate.first_name} {candidate.last_name}" if candidate else "Unknown" return InterviewDetailResponse( interview_id=interview.id, candidate_id=interview.candidate_id, candidate_name=candidate_name, job_id=interview.job_id, start_time=interview.started_at or interview.scheduled_at, status=interview.status, is_active=interview.is_active, websocket_channel_id=interview.websocket_channel_id, overall_score=interview.overall_score, technical_score=interview.technical_score, communication_score=interview.communication_score, questions_asked=interview.questions_asked ) except HTTPException: raise except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error retrieving interview detail: {str(e)}" ) @router.get( "/monitoring/interview/{interview_id}/transcript", response_model=List[TranscriptSegmentResponse], summary="Get interview transcript", description="Retrieve all transcript segments for a specific interview" ) async def get_interview_transcript( interview_id: int, skip: int = Query(0, ge=0, description="Number of segments to skip"), limit: int = Query(1000, ge=1, le=10000, description="Maximum number of segments to return"), db: Session = Depends(get_db), current_user: User = Depends(require_recruiter) ): """ Get transcript segments for an interview. **Requirements:** 5.1, 7.5 **Parameters:** - interview_id: ID of the interview - skip: Number of segments to skip (for pagination) - limit: Maximum number of segments to return **Returns:** List of transcript segments ordered by timestamp, including: - timestamp: Time in seconds from interview start - speaker: 'candidate' or 'interviewer' - text: Transcribed text - confidence: Transcription confidence score **Authorization:** Requires recruiter role or higher """ try: # Verify interview exists interview = db.query(Interview).filter( Interview.id == interview_id ).first() if not interview: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Interview with id {interview_id} not found" ) # Get transcript segments segments = db.query(TranscriptSegment).filter( TranscriptSegment.interview_id == interview_id ).order_by(TranscriptSegment.timestamp).offset(skip).limit(limit).all() return segments except HTTPException: raise except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error retrieving transcript: {str(e)}" ) @router.get( "/monitoring/interview/{interview_id}/emotions", response_model=List[EmotionEventResponse], summary="Get interview emotion events", description="Retrieve all emotion events for a specific interview" ) async def get_interview_emotions( interview_id: int, skip: int = Query(0, ge=0, description="Number of events to skip"), limit: int = Query(1000, ge=1, le=10000, description="Maximum number of events to return"), db: Session = Depends(get_db), current_user: User = Depends(require_recruiter) ): """ Get emotion events for an interview. **Requirements:** 5.2, 7.5 **Parameters:** - interview_id: ID of the interview - skip: Number of events to skip (for pagination) - limit: Maximum number of events to return **Returns:** List of emotion events ordered by timestamp, including: - timestamp: Time in seconds from interview start - emotion_type: Type of emotion detected (confident, nervous, confused, engaged, frustrated) - confidence_score: Overall confidence score - audio_confidence: Confidence from audio analysis - video_confidence: Confidence from video analysis (if available) **Authorization:** Requires recruiter role or higher """ try: # Verify interview exists interview = db.query(Interview).filter( Interview.id == interview_id ).first() if not interview: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Interview with id {interview_id} not found" ) # Get emotion events events = db.query(EmotionEvent).filter( EmotionEvent.interview_id == interview_id ).order_by(EmotionEvent.timestamp).offset(skip).limit(limit).all() return events except HTTPException: raise except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error retrieving emotion events: {str(e)}" )