Spaces:
Sleeping
Sleeping
| # 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") | |
| def on_startup(): | |
| init_db() | |
| class InitiateCallRequest(BaseModel): | |
| candidate_name: str | |
| candidate_phone: str | |
| candidate_email: Optional[str] = None | |
| resume_text: Optional[str] = None | |
| 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"} | |
| 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"]} | |
| 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, | |
| } | |
| 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 | |
| ] | |
| 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() |