Spaces:
Sleeping
Sleeping
| # File: src/call_manager.py | |
| # Purpose: Initiate outbound AI calls via Bland.ai — no webhooks needed, | |
| # Bland.ai handles the full conversation loop internally. | |
| import httpx | |
| from typing import Dict, Any, List | |
| from config import ( | |
| BLAND_API_KEY, BLAND_API_URL, | |
| INTRO_MESSAGE, MAX_QUESTIONS, | |
| ) | |
| # In-memory session store keyed by Bland call_id | |
| _sessions: Dict[str, Dict[str, Any]] = {} | |
| HEADERS = { | |
| "authorization": BLAND_API_KEY, | |
| "Content-Type": "application/json", | |
| } | |
| def initiate_call( | |
| candidate_phone: str, | |
| session_id: int, | |
| candidate_name: str, | |
| resume_skills: List[str], | |
| ) -> str: | |
| """ | |
| Place an outbound AI call via Bland.ai. | |
| Bland handles STT, LLM, TTS and conversation loop internally. | |
| Returns the Bland call_id. | |
| """ | |
| # Build personalised prompt from resume skills | |
| skills_context = "" | |
| if resume_skills: | |
| skills_str = ", ".join(resume_skills[:5]) | |
| skills_context = ( | |
| f"The candidate has experience with: {skills_str}. " | |
| f"Ask relevant questions about these skills during the interview. " | |
| ) | |
| prompt = ( | |
| f"You are an expert AI technical recruiter conducting a phone screening interview with {candidate_name}. " | |
| f"{skills_context}" | |
| "Your job is to: " | |
| "1. Introduce yourself as an AI recruitment assistant. " | |
| "2. Ask one clear question at a time. " | |
| "3. Listen carefully and ask follow-up questions when answers are vague. " | |
| "4. Cover these topics: background and experience, most challenging project, " | |
| "debugging approach, production deployment experience, handling class imbalance, " | |
| "version control experience, and career goals. " | |
| "5. Keep a professional, warm, and encouraging tone. " | |
| "6. After covering all topics, thank the candidate and end the call. " | |
| "Never reveal you are evaluating the candidate. Keep responses under 3 sentences." | |
| ) | |
| payload = { | |
| "phone_number": candidate_phone, | |
| "task": prompt, | |
| "model": "enhanced", | |
| "language": "en", | |
| "voice": "june", | |
| "max_duration": 15, | |
| "answered_by_enabled": True, | |
| "wait_for_greeting": True, | |
| "record": True, | |
| "amd": False, | |
| "interruption_threshold": 100, | |
| "temperature": 0.4, | |
| "webhook": None, | |
| "metadata": { | |
| "session_id": session_id, | |
| "candidate_name": candidate_name, | |
| }, | |
| } | |
| response = httpx.post( | |
| f"{BLAND_API_URL}/calls", | |
| headers=HEADERS, | |
| json=payload, | |
| timeout=30, | |
| ) | |
| response.raise_for_status() | |
| data = response.json() | |
| call_id = data.get("call_id", "") | |
| _sessions[call_id] = { | |
| "session_id": session_id, | |
| "candidate_name": candidate_name, | |
| "status": "active", | |
| } | |
| return call_id | |
| def get_call_status(call_id: str) -> Dict[str, Any]: | |
| """Fetch call status and transcript from Bland.ai.""" | |
| response = httpx.get( | |
| f"{BLAND_API_URL}/calls/{call_id}", | |
| headers=HEADERS, | |
| timeout=30, | |
| ) | |
| response.raise_for_status() | |
| return response.json() | |
| def get_call_transcript(call_id: str) -> List[Dict[str, str]]: | |
| """ | |
| Fetch and format the call transcript from Bland.ai. | |
| Returns list of {speaker, text} dicts. | |
| """ | |
| data = get_call_status(call_id) | |
| transcripts = data.get("transcripts", []) | |
| result = [] | |
| for t in transcripts: | |
| speaker = "AI" if t.get("user") == "assistant" else "Candidate" | |
| text = t.get("text", "").strip() | |
| if text: | |
| result.append({"speaker": speaker, "text": text}) | |
| return result | |
| def get_session(call_id: str) -> Dict[str, Any]: | |
| return _sessions.get(call_id, {}) | |
| def remove_session(call_id: str): | |
| _sessions.pop(call_id, None) |