Spaces:
Sleeping
Sleeping
| """ | |
| modules/llm.py β Phase 1 upgrade | |
| LLM backend: Groq | Model: llama-3.3-70b-versatile | |
| """ | |
| import os | |
| from dotenv import load_dotenv | |
| from groq import Groq | |
| load_dotenv() | |
| GROQ_API_KEY = os.getenv('GROQ_API_KEY') | |
| if not GROQ_API_KEY: | |
| raise ValueError("GROQ_API_KEY not set in environment.") | |
| client = Groq(api_key=GROQ_API_KEY) | |
| # llama3-70b-8192 was decommissioned Aug 2025 β replaced with successor | |
| MODEL = 'llama-3.3-70b-versatile' | |
| # ββ Fixed bookend questions βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| FIRST_QUESTION = "Tell me about yourself and walk me through your background." | |
| LAST_QUESTION = "Where do you see yourself in 5 years, and how does this role fit into that vision?" | |
| # ββ Role-specific system prompts ββββββββββββββββββββββββββββββββββββββββββββββ | |
| ROLE_SYSTEM_PROMPTS = { | |
| "sde": ( | |
| "You are a senior Software Development Engineer conducting a technical interview. " | |
| "Focus on data structures, algorithms, system design, coding practices, and past engineering projects." | |
| ), | |
| "data scientist": ( | |
| "You are a senior Data Scientist conducting a technical interview. " | |
| "Focus on ML/DL concepts, model evaluation, feature engineering, statistics, and past ML projects." | |
| ), | |
| "ml engineer": ( | |
| "You are a senior ML Engineer conducting a technical interview. " | |
| "Focus on model deployment, MLOps, pipelines, scalability, and production ML systems." | |
| ), | |
| "pm": ( | |
| "You are a senior Product Manager conducting a behavioral and strategy interview. " | |
| "Focus on product thinking, prioritization, stakeholder management, and past product decisions." | |
| ), | |
| "default": ( | |
| "You are a professional interviewer conducting a structured interview. " | |
| "Ask thoughtful questions relevant to the candidate's background and target role." | |
| ), | |
| } | |
| def _get_system_prompt(job_role: str) -> str: | |
| key = job_role.lower().strip() | |
| for role_key in ROLE_SYSTEM_PROMPTS: | |
| if role_key in key: | |
| return ROLE_SYSTEM_PROMPTS[role_key] | |
| return ROLE_SYSTEM_PROMPTS["default"] | |
| # ββ Question Generator ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_questions(name: str, job_role: str, experience: str, | |
| skills: str, resume_text: str = '', jd_text: str = '', | |
| difficulty: str = 'Medium', | |
| num_questions: int = 2) -> list[str]: | |
| """ | |
| Generate role-specific interview questions personalised to the candidate. | |
| Returns a list: [FIRST_QUESTION, ...generated..., LAST_QUESTION] | |
| """ | |
| system_prompt = _get_system_prompt(job_role) | |
| resume_section = "" | |
| if resume_text: | |
| resume_section = f"\nResume Highlights:\n{resume_text[:2000]}\n" | |
| jd_section = "" | |
| if jd_text: | |
| jd_section = f"\nJob Description Context:\n{jd_text[:1000]}\n" | |
| diff_prompts = { | |
| "Easy": "Keep questions foundational, focusing on core concepts and straightforward past experiences.", | |
| "Medium": "Include a balance of practical implementation challenges and conceptual understanding.", | |
| "Advance": "Focus heavily on complex edge cases, system bottlenecks, in-depth architectural decisions, and deep domain expertise." | |
| } | |
| diff_instruction = diff_prompts.get(difficulty, diff_prompts["Medium"]) | |
| user_prompt = f"""Generate exactly {num_questions} interview questions for the following candidate. | |
| Return ONLY a numbered list. No explanation, no preamble, no trailing text. | |
| Candidate Profile: | |
| - Name: {name} | |
| - Target Role: {job_role} | |
| - Experience: {experience} | |
| - Key Skills: {skills} | |
| - Difficulty: {difficulty} | |
| {resume_section} | |
| {jd_section} | |
| Rules: | |
| - Difficulty Tuning: {diff_instruction} | |
| - Mix technical and behavioral questions in equal proportion. | |
| - Make each question specific to their background and the provided Job Description context β avoid generic questions. | |
| - Each question should be a single clear sentence.""" | |
| try: | |
| response = client.chat.completions.create( | |
| model=MODEL, | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt}, | |
| ], | |
| temperature=0.7, | |
| ) | |
| raw = (response.choices[0].message.content or "").strip() | |
| lines = [l.strip() for l in raw.split('\n') if l.strip()] | |
| questions = [] | |
| for line in lines: | |
| if line and line[0].isdigit(): | |
| q = line.split('.', 1)[-1].strip() | |
| q = q.split(')', 1)[-1].strip() | |
| if q: | |
| questions.append(q) | |
| questions = questions[:num_questions] | |
| # Pad with fallback if parsing returned fewer than expected | |
| fallbacks = [ | |
| f"Describe a challenging {job_role} project you've worked on.", | |
| f"How do you stay updated with the latest trends in {job_role}?", | |
| ] | |
| while len(questions) < num_questions: | |
| questions.append(fallbacks[len(questions) % len(fallbacks)]) | |
| return [FIRST_QUESTION] + questions + [LAST_QUESTION] | |
| except Exception as e: | |
| print(f"[generate_questions] Error: {e}") | |
| return [ | |
| FIRST_QUESTION, | |
| f"Tell me about a challenging project you've worked on as a {job_role}.", | |
| LAST_QUESTION, | |
| ] | |
| # ββ Follow-up Question Generator βββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_followup(question: str, answer: str, job_role: str) -> str | None: | |
| """ | |
| Given the candidate's answer, decide if a follow-up is warranted. | |
| Returns a follow-up question string, or None if not needed. | |
| """ | |
| system_prompt = _get_system_prompt(job_role) | |
| user_prompt = f"""You are interviewing a candidate for: {job_role} | |
| Original Question: {question} | |
| Candidate Answer: {answer} | |
| Decide: does this answer warrant a follow-up probe? | |
| - If the answer is vague, incomplete, or raises an interesting point worth exploring β return ONE concise follow-up question. | |
| - If the answer is complete and sufficient β return exactly: NO_FOLLOWUP | |
| Return ONLY the follow-up question or NO_FOLLOWUP. Nothing else.""" | |
| try: | |
| response = client.chat.completions.create( | |
| model=MODEL, | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt}, | |
| ], | |
| temperature=0.5, | |
| ) | |
| result = (response.choices[0].message.content or "").strip() | |
| if result == "NO_FOLLOWUP" or not result: | |
| return None | |
| return result | |
| except Exception as e: | |
| print(f"[generate_followup] Error: {e}") | |
| return None | |
| # ββ Answer Evaluator ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def evaluate_answer(question: str, answer: str, job_role: str, | |
| followup: str = '', followup_answer: str = '') -> dict: | |
| """ | |
| Evaluate candidate's answer. Returns a structured dict with score and feedback. | |
| Optionally includes follow-up Q&A in evaluation context. | |
| """ | |
| if not answer or len(answer.strip()) < 5: | |
| return { | |
| "score": 0, | |
| "score_str": "0/10", | |
| "strength": "No answer detected.", | |
| "improvement": "Please speak clearly into the mic.", | |
| "raw": "Score: 0/10\nStrength: No answer detected.\nImprovement: Please speak clearly into the mic.", | |
| } | |
| followup_section = "" | |
| if followup and followup_answer: | |
| followup_section = f"\nFollow-up Question: {followup}\nFollow-up Answer: {followup_answer}\n" | |
| system_prompt = _get_system_prompt(job_role) | |
| user_prompt = f"""You are evaluating a candidate for the role of: {job_role} | |
| Question: {question} | |
| Candidate Answer: {answer}{followup_section} | |
| Evaluate and respond ONLY in this exact format β no extra text: | |
| Score: X/10 | |
| Strength: <one sentence about what they did well> | |
| Improvement: <one specific, actionable suggestion> | |
| Detail: <two sentences of deeper feedback>""" | |
| try: | |
| response = client.chat.completions.create( | |
| model=MODEL, | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt}, | |
| ], | |
| temperature=0.3, | |
| ) | |
| raw = (response.choices[0].message.content or "").strip() | |
| return _parse_evaluation(raw) | |
| except Exception as e: | |
| print(f"[evaluate_answer] Error: {e}") | |
| fallback = "Score: 5/10\nStrength: Answer provided.\nImprovement: Technical issues prevented detailed evaluation.\nDetail: Please retry." | |
| return _parse_evaluation(fallback) | |
| def _parse_evaluation(raw: str) -> dict: | |
| """Parse structured evaluation text into a dict.""" | |
| result = {"raw": raw, "score": 5, "score_str": "5/10", | |
| "strength": "", "improvement": "", "detail": ""} | |
| for line in raw.split('\n'): | |
| if ':' not in line: | |
| continue | |
| key, _, value = line.partition(':') | |
| key = key.strip().lower() | |
| value = value.strip() | |
| if key == "score": | |
| result["score_str"] = value | |
| try: | |
| result["score"] = int(value.split('/')[0]) | |
| except ValueError: | |
| pass | |
| elif key == "strength": | |
| result["strength"] = value | |
| elif key == "improvement": | |
| result["improvement"] = value | |
| elif key == "detail": | |
| result["detail"] = value | |
| return result | |
| # ββ Final Summary Generator βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_final_summary(results: list[dict], job_role: str) -> dict: | |
| """ | |
| Generate overall interview summary from all Q&A results. | |
| Each result dict should have: question, answer, feedback (dict or str). | |
| Returns a structured summary dict. | |
| """ | |
| def feedback_str(r): | |
| fb = r.get('feedback', '') | |
| if isinstance(fb, dict): | |
| return fb.get('raw', '') | |
| return str(fb) | |
| all_feedback = '\n\n'.join([ | |
| f"Q: {r['question']}\nA: {r['answer']}\nFeedback: {feedback_str(r)}" | |
| for r in results | |
| ]) | |
| # Compute average score if evaluations are structured | |
| scores = [] | |
| for r in results: | |
| fb = r.get('feedback', {}) | |
| if isinstance(fb, dict) and 'score' in fb: | |
| scores.append(fb['score']) | |
| avg_score = round(sum(scores) / len(scores), 1) if scores else None | |
| user_prompt = f"""You evaluated a complete mock interview for the role of: {job_role} | |
| Interview Transcript: | |
| {all_feedback} | |
| {'Computed average score across all questions: ' + str(avg_score) + '/10' if avg_score else ''} | |
| Provide a final summary in this EXACT format β no extra text: | |
| Overall Score: X/10 | |
| Top Strength: <one sentence> | |
| Top Area to Improve: <one specific, actionable sentence> | |
| Weak Topics: <comma-separated list of 2-3 topic areas to work on> | |
| Final Tip: <one motivating, specific sentence>""" | |
| try: | |
| response = client.chat.completions.create( | |
| model=MODEL, | |
| messages=[ | |
| {"role": "system", "content": "You are an expert interview coach giving final feedback."}, | |
| {"role": "user", "content": user_prompt}, | |
| ], | |
| temperature=0.4, | |
| ) | |
| raw = (response.choices[0].message.content or "").strip() | |
| return _parse_summary(raw, avg_score) | |
| except Exception as e: | |
| print(f"[generate_final_summary] Error: {e}") | |
| fallback = "Overall Score: 5/10\nTop Strength: Completed the interview.\nTop Area to Improve: Practice clearer answers.\nWeak Topics: Communication, Technical Depth\nFinal Tip: Keep practicing β consistency builds confidence!" | |
| return _parse_summary(fallback, avg_score) | |
| def _parse_summary(raw: str, avg_score=None) -> dict: | |
| """Parse summary text into a structured dict.""" | |
| result = { | |
| "raw": raw, | |
| "overall_score": avg_score or 5, | |
| "overall_score_str": f"{avg_score}/10" if avg_score else "5/10", | |
| "top_strength": "", | |
| "top_area_to_improve": "", | |
| "weak_topics": [], | |
| "final_tip": "", | |
| } | |
| for line in raw.split('\n'): | |
| if ':' not in line: | |
| continue | |
| key, _, value = line.partition(':') | |
| key = key.strip().lower().replace(' ', '_') | |
| value = value.strip() | |
| if key == "overall_score": | |
| result["overall_score_str"] = value | |
| try: | |
| result["overall_score"] = float(value.split('/')[0]) | |
| except ValueError: | |
| pass | |
| elif key == "top_strength": | |
| result["top_strength"] = value | |
| elif key == "top_area_to_improve": | |
| result["top_area_to_improve"] = value | |
| elif key == "weak_topics": | |
| result["weak_topics"] = [t.strip() for t in value.split(',')] | |
| elif key == "final_tip": | |
| result["final_tip"] = value | |
| return result |