Hirely-Backend / backend /api /endpoints /monitoring.py
NaikPranav11's picture
Initial clean deployment
1207440
Raw
History Blame Contribute Delete
11.7 kB
"""
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)}"
)