# File: app.py # Purpose: FastAPI server — recruiter API + Bland.ai call management from typing import Optional from fastapi import FastAPI, Depends, HTTPException, BackgroundTasks from fastapi.responses import Response from pydantic import BaseModel from sqlalchemy.orm import Session import time from src.database import init_db, get_db, Candidate, InterviewSession, ConversationTurn, SessionLocal from src.resume_rag import build_resume_index, extract_skills_from_resume from src.call_manager import ( initiate_call, get_call_status, get_call_transcript, ) from src.evaluator import evaluate_candidate, format_report app = FastAPI(title="AI Interview Screening Caller", version="1.0.0") @app.on_event("startup") def on_startup(): init_db() class InitiateCallRequest(BaseModel): candidate_name: str candidate_phone: str candidate_email: Optional[str] = None resume_text: Optional[str] = None @app.post("/api/initiate-call") async def api_initiate_call( req: InitiateCallRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db), ): candidate = Candidate( name=req.candidate_name, phone=req.candidate_phone, email=req.candidate_email, resume_text=req.resume_text or "", ) db.add(candidate) db.commit() db.refresh(candidate) session = InterviewSession(candidate_id=candidate.id, status="initiated") db.add(session) db.commit() db.refresh(session) skills = [] if req.resume_text: skills = extract_skills_from_resume(req.resume_text) build_resume_index(session.id, req.resume_text) session.extracted_skills = skills db.commit() call_id = initiate_call( candidate_phone=req.candidate_phone, session_id=session.id, candidate_name=req.candidate_name, resume_skills=skills, ) session.call_sid = call_id session.status = "active" db.commit() background_tasks.add_task( poll_call_completion, call_id=call_id, session_id=session.id, candidate_name=req.candidate_name, candidate_phone=req.candidate_phone, ) return {"session_id": session.id, "call_id": call_id, "status": "call_initiated"} @app.post("/api/session/{session_id}/fetch-report") def api_fetch_report(session_id: int, db: Session = Depends(get_db)): """Manually trigger report generation for a session.""" session = db.query(InterviewSession).filter(InterviewSession.id == session_id).first() if not session: raise HTTPException(status_code=404, detail="Session not found") call_id = session.call_sid if not call_id: raise HTTPException(status_code=400, detail="No call ID found for this session") # Fetch status from Bland.ai data = get_call_status(call_id) bland_status = data.get("status", "") transcript = get_call_transcript(call_id) if not transcript: return {"message": f"Call status: {bland_status}. No transcript yet."} # Run evaluation evaluation = evaluate_candidate(session.candidate.name, transcript) report_text = format_report(session.candidate.name, session.candidate.phone, evaluation) session.communication_score = evaluation["communication_score"] session.technical_score = evaluation["technical_score"] session.confidence_score = evaluation["confidence_score"] session.overall_score = evaluation["overall_score"] session.recommendation = evaluation["recommendation"] session.report_text = report_text session.extracted_skills = evaluation.get("skills", []) session.status = "completed" db.commit() # Save transcript existing = db.query(ConversationTurn).filter(ConversationTurn.session_id == session_id).count() if existing == 0: for i, turn in enumerate(transcript): t = ConversationTurn( session_id=session_id, turn_index=i, speaker=turn["speaker"], text=turn["text"], ) db.add(t) db.commit() return {"message": "Report generated successfully", "overall_score": evaluation["overall_score"]} @app.get("/api/session/{session_id}/report") def api_get_report(session_id: int, db: Session = Depends(get_db)): session = db.query(InterviewSession).filter(InterviewSession.id == session_id).first() if not session: raise HTTPException(status_code=404, detail="Session not found") return { "candidate": session.candidate.name, "status": session.status, "call_sid": session.call_sid, "communication_score": session.communication_score, "technical_score": session.technical_score, "confidence_score": session.confidence_score, "overall_score": session.overall_score, "recommendation": session.recommendation, "skills": session.extracted_skills, "report": session.report_text, } @app.get("/api/sessions") def api_list_sessions(db: Session = Depends(get_db)): sessions = db.query(InterviewSession).order_by(InterviewSession.id.desc()).limit(50).all() return [ { "id": s.id, "candidate": s.candidate.name, "status": s.status, "overall_score": s.overall_score, "recommendation": s.recommendation, } for s in sessions ] @app.get("/api/session/{session_id}/transcript") def api_get_transcript(session_id: int, db: Session = Depends(get_db)): turns = db.query(ConversationTurn).filter( ConversationTurn.session_id == session_id ).order_by(ConversationTurn.turn_index).all() return [{"speaker": t.speaker, "text": t.text} for t in turns] def poll_call_completion(call_id, session_id, candidate_name, candidate_phone): db = SessionLocal() try: for _ in range(40): time.sleep(30) data = get_call_status(call_id) status = data.get("status", "") if status in ("complete", "completed"): transcript = get_call_transcript(call_id) if transcript: evaluation = evaluate_candidate(candidate_name, transcript) report_text = format_report(candidate_name, candidate_phone, evaluation) session = db.query(InterviewSession).filter(InterviewSession.id == session_id).first() if session: session.communication_score = evaluation["communication_score"] session.technical_score = evaluation["technical_score"] session.confidence_score = evaluation["confidence_score"] session.overall_score = evaluation["overall_score"] session.recommendation = evaluation["recommendation"] session.report_text = report_text session.extracted_skills = evaluation.get("skills", []) session.status = "completed" db.commit() for i, turn in enumerate(transcript): t = ConversationTurn(session_id=session_id, turn_index=i, speaker=turn["speaker"], text=turn["text"]) db.add(t) db.commit() break elif status in ("failed", "error", "busy", "no-answer"): session = db.query(InterviewSession).filter(InterviewSession.id == session_id).first() if session: session.status = status db.commit() break finally: db.close()