MOSES3377/ai-interview-app
0
1import os2import json3from openai import OpenAI4import logging5 6# Configure logging7logging.basicConfig(level=logging.INFO)8 9# Load environment variables from .env file for local development10try:11 from dotenv import load_dotenv12 load_dotenv()13except ImportError:14 logging.warning("dotenv package not found. Make sure to set environment variables manually.")15 16# Initialize OpenAI client17# It's crucial to have OPENAI_API_KEY set in your environment18try:19 client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))20 OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")21except Exception as e:22 logging.error(f"Failed to initialize OpenAI client: {e}")23 client = None24 25def extract_json_from_string(s: str):26 """27 Safely extracts the first valid JSON object from a string.28 Handles cases where the JSON is embedded within other text.29 """30 try:31 start_index = s.find('{')32 end_index = s.rfind('}') + 133 if start_index != -1 and end_index != -1:34 json_str = s[start_index:end_index]35 return json.loads(json_str)36 except (json.JSONDecodeError, ValueError) as e:37 logging.error(f"JSON decoding failed: {e}")38 return None39 return None40 41def generate_questions(role: str, num_questions: int = 5) -> list:42 """43 Generates a list of interview questions for a given role using an LLM.44 45 Args:46 role (str): The job role for which to generate questions.47 num_questions (int): The number of questions to generate.48 49 Returns:50 list: A list of dictionaries, where each dictionary is a question.51 Returns a static list from questions.json on failure.52 """53 if not client:54 logging.error("OpenAI client not initialized. Falling back to static questions.")55 return load_static_questions()56 57 prompt = f"""58 You are an expert HR interviewer. Generate {num_questions} diverse interview questions for a candidate applying for the role of '{role}'.59 The questions should cover a range of topics, including technical skills, behavioral aspects, problem-solving abilities, and cultural fit.60 Return the output as a clean JSON array of objects, where each object has a single key "text" containing the question.61 62 Example format:63 [64 {{"text": "What is your experience with Python and Django?"}},65 {{"text": "Describe a time you had a conflict with a team member and how you resolved it."}}66 ]67 """68 69 try:70 response = client.chat.completions.create(71 model=OPENAI_MODEL,72 messages=[{"role": "user", "content": prompt}],73 temperature=0.7,74 response_format={"type": "json_object"} # Use JSON mode if available75 )76 content = response.choices[0].message.content77 # The content should already be a JSON object, but we handle cases where it might be wrapped.78 questions_data = json.loads(content)79 # The prompt asks for an array, but the model might return a dictionary with a key.80 # We need to find the list of questions within the returned object.81 if isinstance(questions_data, dict):82 for key, value in questions_data.items():83 if isinstance(value, list):84 return value # Return the first list found85 elif isinstance(questions_data, list):86 return questions_data87 88 logging.error("LLM returned unexpected JSON structure. Falling back to static questions.")89 return load_static_questions()90 91 except Exception as e:92 logging.error(f"Error generating questions with LLM: {e}")93 return load_static_questions()94 95def evaluate_answer(question: str, answer: str) -> dict:96 """97 Evaluates a candidate's answer to a question using an LLM.98 99 Args:100 question (str): The interview question that was asked.101 answer (str): The candidate's transcribed answer.102 103 Returns:104 dict: A dictionary containing the score, feedback, and a suggested better answer.105 """106 if not client or not answer:107 return {"score": 0, "feedback": "Evaluation could not be performed.", "better_answer": "N/A"}108 109 prompt = f"""110 As an expert interviewer, evaluate the following answer to an interview question.111 Provide a constructive, encouraging, and brief feedback.112 Also, provide a score from 0 to 10, where 0 is very poor and 10 is excellent.113 Finally, provide an improved, concise version of the answer that would be considered ideal.114 115 Question: "{question}"116 Candidate's Answer: "{answer}"117 118 Return your evaluation as a clean JSON object with three keys: "score", "feedback", and "better_answer".119 Example format:120 {{121 "score": 8,122 "feedback": "This is a strong answer that clearly demonstrates your skills. You could make it even better by providing a more specific metric of your success.",123 "better_answer": "In my previous role, I led a project that increased user engagement by 15% in one quarter by implementing a new recommendation algorithm."124 }}125 """126 try:127 response = client.chat.completions.create(128 model=OPENAI_MODEL,129 messages=[{"role": "user", "content": prompt}],130 temperature=0.5,131 response_format={"type": "json_object"}132 )133 content = response.choices[0].message.content134 evaluation = json.loads(content)135 return evaluation136 except Exception as e:137 logging.error(f"Error evaluating answer with LLM: {e}")138 return {"score": 0, "feedback": "An error occurred during evaluation.", "better_answer": "Could not be generated."}139 140 141def get_interview_summary(evaluations: list) -> dict:142 """143 Generates a final summary of the interview based on all evaluations.144 145 Args:146 evaluations (list): A list of evaluation dictionaries for each answer.147 148 Returns:149 dict: A dictionary containing the final score and a summary paragraph.150 """151 if not client or not evaluations:152 return {"final_score": 0, "summary": "Could not generate a summary."}153 154 # Calculate average score155 total_score = sum(e.get('score', 0) for e in evaluations)156 num_questions = len(evaluations)157 final_score = round(total_score / num_questions, 1) if num_questions > 0 else 0158 159 # Prepare context for summary generation160 transcript = "\n\n".join(161 f"Question {i+1}: {e['question']}\nAnswer: {e['answer']}\nFeedback: {e['feedback']} (Score: {e['score']})"162 for i, e in enumerate(evaluations)163 )164 165 prompt = f"""166 Based on the following interview transcript and evaluations, provide a brief, overall summary of the candidate's performance.167 Highlight one key strength and one area for improvement. Keep the tone professional and constructive.168 Do not mention the final score in your summary text.169 170 Transcript:171 {transcript}172 173 Return a single JSON object with the key "summary".174 """175 176 try:177 response = client.chat.completions.create(178 model=OPENAI_MODEL,179 messages=[{"role": "user", "content": prompt}],180 temperature=0.6,181 response_format={"type": "json_object"}182 )183 content = response.choices[0].message.content184 summary_data = json.loads(content)185 return {"final_score": final_score, "summary": summary_data.get("summary", "Summary could not be generated.")}186 187 except Exception as e:188 logging.error(f"Error generating summary with LLM: {e}")189 return {"final_score": final_score, "summary": "An error occurred while generating the final summary."}190 191def load_static_questions() -> list:192 """Loads the fallback questions from the local JSON file."""193 try:194 with open("questions.json", "r") as f:195 return json.load(f)196 except (FileNotFoundError, json.JSONDecodeError):197 # A hardcoded ultimate fallback198 return [{"text": "Tell me about yourself."}]199 