Spaces:
Sleeping
Sleeping
| # File: src/interview_agent.py | |
| # Purpose: Stateful LLM interview agent using Groq (llama-3.3-70b-versatile) | |
| from typing import List, Dict, Optional | |
| from groq import Groq | |
| from config import ( | |
| GROQ_API_KEY, GROQ_LLM_MODEL, LLM_TEMPERATURE, LLM_MAX_TOKENS, | |
| MAX_QUESTIONS, INTRO_MESSAGE | |
| ) | |
| from src.resume_rag import retrieve_resume_context | |
| _groq = Groq(api_key=GROQ_API_KEY) | |
| SYSTEM_PROMPT = """You are an expert AI technical recruiter conducting a structured phone interview. | |
| Your job is to: | |
| 1. Ask one clear question at a time — never ask multiple questions together. | |
| 2. Listen carefully to the candidate's answer, identify skills and technical depth. | |
| 3. Ask a natural follow-up if the answer is vague or incomplete. | |
| 4. Move to the next planned question when the current topic is sufficiently explored. | |
| 5. Keep a professional, warm, and encouraging tone. | |
| Rules: | |
| - Never reveal you are evaluating the candidate. | |
| - Do not provide hints or correct the candidate. | |
| - Keep responses under 3 sentences. | |
| - If the candidate is off-topic, gently redirect. | |
| """ | |
| BASE_QUESTIONS = [ | |
| "Can you start by giving me a brief overview of your background and what you've been working on recently?", | |
| "What is the most technically challenging project you have worked on, and what was your specific contribution?", | |
| "How do you approach debugging a model that is underperforming on a validation set?", | |
| "Describe your experience with deploying machine learning models to production.", | |
| "How do you handle class imbalance in a classification problem?", | |
| "What is your experience with version control and collaborative development?", | |
| "Where do you see yourself growing technically in the next year?", | |
| ] | |
| class InterviewAgent: | |
| def __init__( | |
| self, | |
| session_id: int, | |
| candidate_name: str, | |
| resume_skills: List[str], | |
| use_rag: bool = True, | |
| ): | |
| self.session_id = session_id | |
| self.candidate_name = candidate_name | |
| self.resume_skills = resume_skills | |
| self.use_rag = use_rag | |
| self.history: List[Dict[str, str]] = [] | |
| self.question_index = 0 | |
| self.turn_count = 0 | |
| self.is_complete = False | |
| self._build_question_plan() | |
| def _build_question_plan(self): | |
| personalised = [] | |
| for skill in self.resume_skills[:3]: | |
| personalised.append( | |
| f"I noticed your experience with {skill}. Can you walk me through a specific project where you used it?" | |
| ) | |
| self.questions = (personalised + BASE_QUESTIONS)[:MAX_QUESTIONS] | |
| def _get_resume_context(self, last_answer: str) -> str: | |
| if not self.use_rag: | |
| return "" | |
| return retrieve_resume_context(self.session_id, last_answer) | |
| def get_next_ai_message(self, candidate_answer: Optional[str] = None) -> str: | |
| if candidate_answer is not None: | |
| self.history.append({"role": "user", "content": candidate_answer}) | |
| self.turn_count += 1 | |
| if self.question_index >= len(self.questions): | |
| self.is_complete = True | |
| closing = ( | |
| f"Thank you so much, {self.candidate_name}. That concludes our screening interview. " | |
| "The hiring team will review your responses and reach out with next steps. " | |
| "Have a great day!" | |
| ) | |
| self.history.append({"role": "assistant", "content": closing}) | |
| return closing | |
| resume_ctx = "" | |
| if candidate_answer: | |
| resume_ctx = self._get_resume_context(candidate_answer) | |
| next_question = self.questions[self.question_index] | |
| system_with_context = SYSTEM_PROMPT | |
| if resume_ctx: | |
| system_with_context += f"\n\nRelevant resume context:\n{resume_ctx}" | |
| system_with_context += f"\n\nNext planned question: {next_question}" | |
| system_with_context += ( | |
| "\n\nIf the last answer was incomplete, ask a short follow-up first. " | |
| "Otherwise transition naturally and ask the next planned question." | |
| ) | |
| messages = [{"role": "system", "content": system_with_context}] + self.history | |
| response = _groq.chat.completions.create( | |
| model=GROQ_LLM_MODEL, | |
| messages=messages, | |
| temperature=LLM_TEMPERATURE, | |
| max_tokens=LLM_MAX_TOKENS, | |
| ) | |
| ai_text = response.choices[0].message.content.strip() | |
| if candidate_answer is not None: | |
| self.question_index += 1 | |
| self.history.append({"role": "assistant", "content": ai_text}) | |
| return ai_text | |
| def get_full_transcript(self) -> List[Dict[str, str]]: | |
| transcript = [] | |
| for msg in self.history: | |
| speaker = "AI" if msg["role"] == "assistant" else "Candidate" | |
| transcript.append({"speaker": speaker, "text": msg["content"]}) | |
| return transcript |