Spaces:
Runtime error
Runtime error
| """ | |
| 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 | |
| # ============================================================================ | |
| 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)}" | |
| ) | |
| 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)}" | |
| ) | |
| 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)}" | |
| ) | |
| 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)}" | |
| ) | |