madi7a commited on
Commit
d90a0a5
·
1 Parent(s): 56bb51e

feat: Add core application files and correct gitignore

Browse files
Files changed (5) hide show
  1. .gitattributes +1 -0
  2. .gitignore +5 -0
  3. app.py +368 -0
  4. rag.py +307 -0
  5. requirements.txt +50 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Ignore environment variables file
2
+ .env
3
+
4
+ # Python cache
5
+ __pycache__/
app.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import time
3
+ import torch
4
+ import tempfile
5
+ import numpy as np
6
+ import scipy.io.wavfile as wavfile
7
+
8
+ from transformers import AutoProcessor, BarkModel
9
+ import whisper
10
+
11
+
12
+
13
+ import gradio as gr
14
+ import time
15
+ import tempfile
16
+ import numpy as np
17
+ import scipy.io.wavfile as wavfile
18
+ import cv2
19
+ import os
20
+ import json
21
+ from moviepy.editor import VideoFileClip
22
+ import shutil
23
+
24
+ # Bark TTS
25
+ model_bark = BarkModel.from_pretrained("suno/bark")
26
+ processor_bark = AutoProcessor.from_pretrained("suno/bark")
27
+ model_bark.to("cuda" if torch.cuda.is_available() else "cpu")
28
+ bark_voice_preset = "v2/en_speaker_6"
29
+
30
+ def bark_tts(text):
31
+ inputs = processor_bark(text, return_tensors="pt", voice_preset=bark_voice_preset)
32
+ inputs = {k: v.to(model_bark.device) for k, v in inputs.items()}
33
+ speech_values = model_bark.generate(**inputs)
34
+ speech = speech_values.cpu().numpy().squeeze()
35
+ speech = (speech * 32767).astype(np.int16)
36
+ temp_wav = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
37
+ wavfile.write(temp_wav.name, 22050, speech)
38
+ return temp_wav.name
39
+
40
+ # Whisper STT
41
+ whisper_model = whisper.load_model("base")
42
+ def whisper_stt(audio_path):
43
+ if not audio_path or not os.path.exists(audio_path): return ""
44
+ result = whisper_model.transcribe(audio_path)
45
+ return result["text"]
46
+
47
+
48
+ # DeepFace (Video Face Emotion)
49
+ def ensure_mp4(video_input):
50
+ # video_input could be a file-like object, a path, or a Gradio temp path
51
+ if isinstance(video_input, str):
52
+ input_path = video_input
53
+ else:
54
+ # It's a file-like object (rare for Gradio video, but handle it)
55
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".webm") as temp_in:
56
+ temp_in.write(video_input.read())
57
+ input_path = temp_in.name
58
+
59
+ # If already mp4, return as is
60
+ if input_path.endswith(".mp4"):
61
+ return input_path
62
+
63
+ # Convert to mp4 using moviepy
64
+ mp4_path = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
65
+ try:
66
+ clip = VideoFileClip(input_path)
67
+ clip.write_videofile(mp4_path, codec="libx264", audio=False, verbose=False, logger=None)
68
+ clip.close()
69
+ except Exception as e:
70
+ print("Video conversion failed:", e)
71
+ # As fallback, just copy original
72
+ shutil.copy(input_path, mp4_path)
73
+ return mp4_path
74
+
75
+ def analyze_video_emotions(video_input, sample_rate=15):
76
+ # Convert input to an mp4 file OpenCV can process
77
+ mp4_path = ensure_mp4(video_input)
78
+ if not mp4_path or not os.path.exists(mp4_path):
79
+ return "neutral"
80
+ cap = cv2.VideoCapture(mp4_path)
81
+ frame_count = 0
82
+ emotion_counts = {}
83
+ while True:
84
+ ret, frame = cap.read()
85
+ if not ret: break
86
+ if frame_count % sample_rate == 0:
87
+ try:
88
+ result = DeepFace.analyze(frame, actions=['emotion'], enforce_detection=False)
89
+ dominant = result[0]["dominant_emotion"] if isinstance(result, list) else result["dominant_emotion"]
90
+ emotion_counts[dominant] = emotion_counts.get(dominant, 0) + 1
91
+ except Exception: pass
92
+ frame_count += 1
93
+ cap.release()
94
+ if not emotion_counts: return "neutral"
95
+ return max(emotion_counts.items(), key=lambda x: x[1])[0]
96
+
97
+ wav2vec_model_name = "HaniaRuby/speech-emotion-recognition-wav2vec2"
98
+ wav2vec_processor = Wav2Vec2Processor.from_pretrained(wav2vec_model_name)
99
+ wav2vec_model = Wav2Vec2ForSequenceClassification.from_pretrained(wav2vec_model_name)
100
+ wav2vec_model.eval()
101
+ voice_label_map = {
102
+ 0: 'angry', 1: 'disgust', 2: 'fear', 3: 'happy',
103
+ 4: 'neutral', 5: 'sad', 6: 'surprise'
104
+ }
105
+
106
+
107
+
108
+ def analyze_audio_emotion(audio_path):
109
+ if not audio_path or not os.path.exists(audio_path): return "neutral"
110
+ speech, sr = librosa.load(audio_path, sr=16000)
111
+ inputs = wav2vec_processor(speech, sampling_rate=16000, return_tensors="pt")
112
+ with torch.no_grad():
113
+ logits = wav2vec_model(**inputs).logits
114
+ probs = torch.nn.functional.softmax(logits, dim=-1)
115
+ predicted_id = torch.argmax(probs, dim=-1).item()
116
+ return voice_label_map.get(predicted_id, "neutral")
117
+
118
+ # --- Effective confidence calculation
119
+ def interpret_confidence(voice_label, face_label, answer_score_label, k=0.2):
120
+ emotion_map = {"happy": 0.9, "neutral": 0.6, "surprised": 0.7, "sad": 0.4, "angry": 0.3, "disgust": 0.2, "fear": 0.3, "no_face": 0.5, "unknown": 0.5}
121
+ answer_score_map = {"excellent": 1.0, "good": 0.8, "medium": 0.6, "poor": 0.3}
122
+ voice_score, face_score, answer_score = emotion_map.get(voice_label, 0.5), emotion_map.get(face_label, 0.5), answer_score_map.get(answer_score_label, 0.5)
123
+ avg_emotion = (voice_score + face_score) / 2
124
+ control_bonus = max(0, answer_score - avg_emotion) * k
125
+ eff_conf = (0.5 * answer_score + 0.22 * voice_score + 0.18 * face_score + 0.1 * control_bonus)
126
+ return {"effective_confidence": round(eff_conf, 3), "answer_score": round(answer_score, 2), "voice_score": round(voice_score, 2), "face_score": round(face_score, 2), "control_bonus": round(control_bonus, 3)}
127
+
128
+ seniority_mapping = {
129
+ "Entry-level": 1, "Junior": 2, "Mid-Level": 3, "Senior": 4, "Lead": 5
130
+ }
131
+ import gradio as gr
132
+ import time
133
+ import tempfile
134
+ import numpy as np
135
+ import scipy.io.wavfile as wavfile
136
+ import cv2
137
+ import os
138
+ import json
139
+
140
+
141
+
142
+ # --- 2. Gradio App ---
143
+
144
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
145
+ user_data = gr.State({})
146
+ interview_state = gr.State({})
147
+ missing_fields_state = gr.State([])
148
+
149
+ # --- UI Layout ---
150
+ with gr.Column(visible=True) as user_info_section:
151
+ gr.Markdown("## Candidate Information")
152
+ cv_file = gr.File(label="Upload CV")
153
+ job_desc = gr.Textbox(label="Job Description")
154
+ start_btn = gr.Button("Continue", interactive=False)
155
+
156
+ with gr.Column(visible=False) as missing_section:
157
+ gr.Markdown("## Missing Information")
158
+ name_in = gr.Textbox(label="Name", visible=False)
159
+ role_in = gr.Textbox(label="Job Role", visible=False)
160
+ seniority_in = gr.Dropdown(list(seniority_mapping.keys()), label="Seniority", visible=False)
161
+ skills_in = gr.Textbox(label="Skills", visible=False)
162
+ submit_btn = gr.Button("Submit", interactive=False)
163
+
164
+ with gr.Column(visible=False) as interview_pre_section:
165
+ pre_interview_greeting_md = gr.Markdown()
166
+ start_interview_final_btn = gr.Button("Start Interview")
167
+
168
+ with gr.Column(visible=False) as interview_section:
169
+ gr.Markdown("## Interview in Progress")
170
+ question_audio = gr.Audio(label="Listen", interactive=False, autoplay=True)
171
+ question_text = gr.Markdown()
172
+ user_audio_input = gr.Audio(sources=["microphone"], type="filepath", label="1. Record Audio Answer")
173
+ user_video_input = gr.Video(sources=["webcam"], label="2. Record Video Answer")
174
+ stt_transcript = gr.Textbox(label="Transcribed Answer (edit if needed)")
175
+ confirm_btn = gr.Button("Confirm Answer")
176
+ evaluation_display = gr.Markdown()
177
+ emotion_display = gr.Markdown()
178
+ interview_summary = gr.Markdown(visible=False)
179
+
180
+ # --- UI Logic ---
181
+
182
+ def validate_start_btn(cv_file, job_desc):
183
+ return gr.update(interactive=(cv_file is not None and hasattr(cv_file, "name") and bool(job_desc and job_desc.strip())))
184
+ cv_file.change(validate_start_btn, [cv_file, job_desc], start_btn)
185
+ job_desc.change(validate_start_btn, [cv_file, job_desc], start_btn)
186
+
187
+ def process_and_route_initial(cv_file, job_desc):
188
+ details = extract_candidate_details(cv_file.name)
189
+ job_info = extract_job_details(job_desc)
190
+ data = {
191
+ "name": details.get("name", "unknown"), "job_role": job_info.get("job_title", "unknown"),
192
+ "seniority": job_info.get("experience_level", "unknown"), "skills": job_info.get("skills", [])
193
+ }
194
+ missing = [k for k, v in data.items() if (isinstance(v, str) and v.lower() == "unknown") or not v]
195
+ if missing:
196
+ return data, missing, gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
197
+ else:
198
+ greeting = f"Hello {data['name']}, your profile is ready. Click 'Start Interview' when ready."
199
+ return data, missing, gr.update(visible=False), gr.update(visible=False), gr.update(visible=True, value=greeting)
200
+ start_btn.click(
201
+ process_and_route_initial,
202
+ [cv_file, job_desc],
203
+ [user_data, missing_fields_state, user_info_section, missing_section, pre_interview_greeting_md]
204
+ )
205
+
206
+ def show_missing(missing):
207
+ if missing is None: missing = []
208
+ return gr.update(visible="name" in missing), gr.update(visible="job_role" in missing), gr.update(visible="seniority" in missing), gr.update(visible="skills" in missing)
209
+ missing_fields_state.change(show_missing, missing_fields_state, [name_in, role_in, seniority_in, skills_in])
210
+
211
+ def validate_fields(name, role, seniority, skills, missing):
212
+ if not missing: return gr.update(interactive=False)
213
+ all_filled = all([(not ("name" in missing) or bool(name.strip())), (not ("job_role" in missing) or bool(role.strip())), (not ("seniority" in missing) or bool(seniority)), (not ("skills" in missing) or bool(skills.strip())),])
214
+ return gr.update(interactive=all_filled)
215
+ for inp in [name_in, role_in, seniority_in, skills_in]:
216
+ inp.change(validate_fields, [name_in, role_in, seniority_in, skills_in, missing_fields_state], submit_btn)
217
+
218
+ def complete_manual(data, name, role, seniority, skills):
219
+ if data["name"].lower() == "unknown": data["name"] = name
220
+ if data["job_role"].lower() == "unknown": data["job_role"] = role
221
+ if data["seniority"].lower() == "unknown": data["seniority"] = seniority
222
+ if not data["skills"]: data["skills"] = [s.strip() for s in skills.split(",")]
223
+ greeting = f"Hello {data['name']}, your profile is ready. Click 'Start Interview' to begin."
224
+ return data, gr.update(visible=False), gr.update(visible=True), gr.update(value=greeting)
225
+ submit_btn.click(complete_manual, [user_data, name_in, role_in, seniority_in, skills_in], [user_data, missing_section, interview_pre_section, pre_interview_greeting_md])
226
+
227
+ def start_interview(data):
228
+ # --- Advanced state with full logging ---
229
+ state = {
230
+ "questions": [], "answers": [], "face_labels": [], "voice_labels": [], "timings": [],
231
+ "question_evaluations": [], "answer_evaluations": [], "effective_confidences": [],
232
+ "conversation_history": [],
233
+ "difficulty_adjustment": None,
234
+ "question_idx": 0, "max_questions": 3, "q_start_time": time.time(),
235
+ "log": []
236
+ }
237
+ # --- Optionally: context retrieval here (currently just blank) ---
238
+ context = ""
239
+ prompt = build_interview_prompt(
240
+ conversation_history=[], user_response="", context=context, job_role=data["job_role"],
241
+ skills=data["skills"], seniority=data["seniority"], difficulty_adjustment=None,
242
+ voice_label="neutral", face_label="neutral"
243
+ )
244
+ first_q = groq_llm.predict(prompt)
245
+ # Evaluate Q for quality
246
+ q_eval = eval_question_quality(first_q, data["job_role"], data["seniority"], None)
247
+ state["questions"].append(first_q)
248
+ state["question_evaluations"].append(q_eval)
249
+ state["conversation_history"].append({'role': 'Interviewer', 'content': first_q})
250
+ audio_path = bark_tts(first_q)
251
+ # LOG
252
+ state["log"].append({"type": "question", "question": first_q, "question_eval": q_eval, "timestamp": time.time()})
253
+ return state, gr.update(visible=False), gr.update(visible=True), audio_path, f"*Question 1:* {first_q}"
254
+ start_interview_final_btn.click(start_interview, [user_data], [interview_state, interview_pre_section, interview_section, question_audio, question_text])
255
+
256
+ def transcribe(audio_path):
257
+ return whisper_stt(audio_path)
258
+ user_audio_input.change(transcribe, user_audio_input, stt_transcript)
259
+
260
+ def process_answer(transcript, audio_path, video_path, state, data):
261
+ if not transcript and not video_path:
262
+ return state, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
263
+ elapsed = round(time.time() - state.get("q_start_time", time.time()), 2)
264
+ state["timings"].append(elapsed)
265
+ state["answers"].append(transcript)
266
+ state["conversation_history"].append({'role': 'Candidate', 'content': transcript})
267
+
268
+ # --- 1. Emotion analysis ---
269
+ voice_label = analyze_audio_emotion(audio_path)
270
+ face_label = analyze_video_emotions(video_path)
271
+ state["voice_labels"].append(voice_label)
272
+ state["face_labels"].append(face_label)
273
+
274
+ # --- 2. Evaluate previous Q and Answer ---
275
+ last_q = state["questions"][-1]
276
+ q_eval = state["question_evaluations"][-1] # Already in state
277
+ ref_answer = generate_reference_answer(last_q, data["job_role"], data["seniority"])
278
+ answer_eval = evaluate_answer(last_q, transcript, ref_answer, data["job_role"], data["seniority"], None)
279
+ state["answer_evaluations"].append(answer_eval)
280
+ answer_score = answer_eval.get("Score", "medium") if answer_eval else "medium"
281
+
282
+ # --- 3. Adaptive difficulty ---
283
+ if answer_score == "excellent":
284
+ state["difficulty_adjustment"] = "harder"
285
+ elif answer_score in ("medium", "poor"):
286
+ state["difficulty_adjustment"] = "easier"
287
+ else:
288
+ state["difficulty_adjustment"] = None
289
+
290
+ # --- 4. Effective confidence ---
291
+ eff_conf = interpret_confidence(voice_label, face_label, answer_score)
292
+ state["effective_confidences"].append(eff_conf)
293
+
294
+ # --- LOG ---
295
+ state["log"].append({
296
+ "type": "answer",
297
+ "question": last_q,
298
+ "answer": transcript,
299
+ "answer_eval": answer_eval,
300
+ "ref_answer": ref_answer,
301
+ "face_label": face_label,
302
+ "voice_label": voice_label,
303
+ "effective_confidence": eff_conf,
304
+ "timing": elapsed,
305
+ "timestamp": time.time()
306
+ })
307
+
308
+ # --- Next or End ---
309
+ qidx = state["question_idx"] + 1
310
+ if qidx >= state["max_questions"]:
311
+ # Save as JSON (optionally)
312
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
313
+ log_file = f"interview_log_{timestamp}.json"
314
+ with open(log_file, "w", encoding="utf-8") as f:
315
+ json.dump(state["log"], f, indent=2, ensure_ascii=False)
316
+ # Report
317
+ summary = "# Interview Summary\n"
318
+ for i, q in enumerate(state["questions"]):
319
+ summary += (f"\n### Q{i + 1}: {q}\n"
320
+ f"- *Answer*: {state['answers'][i]}\n"
321
+ f"- *Q Eval*: {state['question_evaluations'][i]}\n"
322
+ f"- *A Eval*: {state['answer_evaluations'][i]}\n"
323
+ f"- *Face Emotion: {state['face_labels'][i]}, **Voice Emotion*: {state['voice_labels'][i]}\n"
324
+ f"- *Effective Confidence*: {state['effective_confidences'][i]['effective_confidence']}\n"
325
+ f"- *Time*: {state['timings'][i]}s\n")
326
+ summary += f"\n\n⏺ Full log saved as {log_file}."
327
+ return (state, gr.update(visible=True, value=summary), gr.update(value=None), gr.update(value=None), gr.update(value=None), gr.update(value=None), gr.update(visible=True, value=f"Last Detected — Face: {face_label}, Voice: {voice_label}"))
328
+ else:
329
+ # --- Build next prompt using adaptive difficulty ---
330
+ state["question_idx"] = qidx
331
+ state["q_start_time"] = time.time()
332
+ context = "" # You can add your context logic here
333
+ prompt = build_interview_prompt(
334
+ conversation_history=state["conversation_history"],
335
+ user_response=transcript,
336
+ context=context,
337
+ job_role=data["job_role"],
338
+ skills=data["skills"],
339
+ seniority=data["seniority"],
340
+ difficulty_adjustment=state["difficulty_adjustment"],
341
+ face_label=face_label,
342
+ voice_label=voice_label,
343
+ effective_confidence=eff_conf
344
+ )
345
+ next_q = groq_llm.predict(prompt)
346
+ # Evaluate Q quality
347
+ q_eval = eval_question_quality(next_q, data["job_role"], data["seniority"], None)
348
+ state["questions"].append(next_q)
349
+ state["question_evaluations"].append(q_eval)
350
+ state["conversation_history"].append({'role': 'Interviewer', 'content': next_q})
351
+ state["log"].append({"type": "question", "question": next_q, "question_eval": q_eval, "timestamp": time.time()})
352
+ audio_path = bark_tts(next_q)
353
+ # Display evaluations
354
+ eval_md = f"*Last Answer Eval:* {answer_eval}\n\n*Effective Confidence:* {eff_conf}"
355
+ return (
356
+ state, gr.update(visible=False), audio_path, f"*Question {qidx + 1}:* {next_q}",
357
+ gr.update(value=None), gr.update(value=None),
358
+ gr.update(visible=True, value=f"Last Detected — Face: {face_label}, Voice: {voice_label}"),
359
+ )
360
+ confirm_btn.click(
361
+ process_answer,
362
+ [stt_transcript, user_audio_input, user_video_input, interview_state, user_data],
363
+ [interview_state, interview_summary, question_audio, question_text, user_audio_input, user_video_input, emotion_display]
364
+ ).then(
365
+ lambda: (gr.update(value=None), gr.update(value=None)), None, [user_audio_input, user_video_input]
366
+ )
367
+
368
+ demo.launch()
rag.py ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import time
5
+ import random
6
+ import logging
7
+ import traceback
8
+ from collections import defaultdict
9
+ from enum import Enum
10
+ from typing import Dict
11
+
12
+ # --- .env for secrets ---
13
+ from dotenv import load_dotenv
14
+
15
+ # --- LangChain & Hugging Face---
16
+ # Note: Some of these imports might be from older versions of LangChain.
17
+ # Ensure your dependencies match.
18
+ from langchain_groq import ChatGroq as LangChainChatGroq # Renamed to avoid conflict
19
+ from langchain_community.embeddings import HuggingFaceEmbeddings
20
+ from langchain_community.vectorstores import Qdrant
21
+ from langchain.prompts import PromptTemplate
22
+ from langchain.chains import LLMChain
23
+ from langchain.retrievers import ContextualCompressionRetriever
24
+ from langchain.retrievers.document_compressors import CohereRerank
25
+ from huggingface_hub import login
26
+
27
+ # --- Qdrant Vector DB ---
28
+ from qdrant_client import QdrantClient
29
+ from qdrant_client.http.models import (
30
+ VectorParams, Distance, Filter, FieldCondition, MatchValue,
31
+ PointStruct
32
+ )
33
+
34
+ # --- Models, Embeddings, and Utilities ---
35
+ import cohere
36
+ from sentence_transformers import SentenceTransformer
37
+ import torch
38
+ from transformers import (
39
+ pipeline, AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
40
+ )
41
+
42
+ # --- Utility ---
43
+ import numpy as np
44
+ from sklearn.metrics.pairwise import cosine_similarity
45
+ from textwrap import dedent
46
+ import requests
47
+ from docx import Document
48
+ import textract
49
+ from PyPDF2 import PdfReader
50
+
51
+
52
+ # ==============================================================================
53
+ # 1. SCRIPT CONFIGURATION
54
+ # ==============================================================================
55
+ # Configure logging
56
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
57
+
58
+ # --- Hugging Face Model for Local Evaluation ---
59
+ JUDGE_MODEL_NAME = "mistralai/Mistral-7B-Instruct-v0.3"
60
+ EMBEDDING_MODEL_NAME = "all-MiniLM-L6-v2"
61
+ QDRANT_COLLECTION_NAME = "interview_questions"
62
+
63
+
64
+ # ==============================================================================
65
+ # 2. API AND ENVIRONMENT HANDLING
66
+ # ==============================================================================
67
+ def handle_apis():
68
+ """
69
+ Loads API keys from a .env file, validates them, and logs into Hugging Face.
70
+
71
+ This function is the single entry point for handling all external secrets.
72
+ It will raise a ValueError if any required key is not found, stopping the
73
+ script from running with a misconfiguration.
74
+ """
75
+ load_dotenv()
76
+ logging.info("Attempting to load API keys from .env file...")
77
+
78
+ required_vars = [
79
+ "GROQ_API_KEY",
80
+ "QDRANT_API_KEY",
81
+ "QDRANT_API_URL",
82
+ "COHERE_API_KEY",
83
+ "HF_API_KEY"
84
+ ]
85
+ missing_vars = [var for var in required_vars if not os.getenv(var)]
86
+
87
+ if missing_vars:
88
+ error_message = (
89
+ f"Error: Missing required environment variables: {', '.join(missing_vars)}. "
90
+ "Please create a .env file in the root directory with all necessary keys."
91
+ )
92
+ logging.critical(error_message)
93
+ raise ValueError(error_message)
94
+
95
+ logging.info("✅ Successfully loaded and validated all required API keys.")
96
+
97
+ try:
98
+ hf_api_key = os.getenv("HF_API_KEY")
99
+ login(token=hf_api_key)
100
+ logging.info("✅ Successfully logged into Hugging Face Hub.")
101
+ except Exception as e:
102
+ error_message = f"Failed to log in to Hugging Face Hub. Please check your HF_API_KEY. Error: {e}"
103
+ logging.critical(error_message)
104
+ raise RuntimeError(error_message)
105
+
106
+ # --- Run the API handler at the start of the script ---
107
+ handle_apis()
108
+
109
+
110
+ # ==============================================================================
111
+ # 3. INITIALIZE API CLIENTS AND MODELS
112
+ # ==============================================================================
113
+ # --- Load API keys from environment (now that they are validated) ---
114
+ chat_groq_api = os.getenv("GROQ_API_KEY")
115
+ qdrant_api = os.getenv("QDRANT_API_KEY")
116
+ qdrant_url = os.getenv("QDRANT_API_URL")
117
+ cohere_api_key = os.getenv("COHERE_API_KEY")
118
+
119
+ # --- Initialize API Clients ---
120
+ logging.info("Initializing API clients...")
121
+ qdrant_client = QdrantClient(url=qdrant_url, api_key=qdrant_api)
122
+ cohere_client = cohere.Client(api_key=cohere_api_key)
123
+ logging.info("✅ API clients initialized.")
124
+
125
+
126
+ # --- Custom ChatGroq Class (if not using LangChain's native one) ---
127
+ class ChatGroq:
128
+ def __init__(self, temperature, model_name, api_key):
129
+ self.temperature = temperature
130
+ self.model_name = model_name
131
+ self.api_key = api_key
132
+ self.api_url = "https://api.groq.com/openai/v1/chat/completions"
133
+
134
+ def predict(self, prompt):
135
+ try:
136
+ headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
137
+ payload = {
138
+ "model": self.model_name,
139
+ "messages": [{"role": "system", "content": "You are an AI interviewer."},
140
+ {"role": "user", "content": prompt}],
141
+ "temperature": self.temperature,
142
+ "max_tokens": 1024 # Increased for longer reports
143
+ }
144
+ response = requests.post(self.api_url, headers=headers, json=payload, timeout=20)
145
+ response.raise_for_status()
146
+ data = response.json()
147
+ if "choices" in data and len(data["choices"]) > 0:
148
+ return data["choices"][0]["message"]["content"].strip()
149
+ logging.warning("Unexpected response structure from Groq API")
150
+ return "Interviewer: Could you tell me more about your relevant experience?"
151
+ except requests.exceptions.RequestException as e:
152
+ logging.error(f"ChatGroq API error: {e}")
153
+ return "Interviewer: Due to a system issue, let's move on to another question."
154
+
155
+ groq_llm = ChatGroq(temperature=0.7, model_name="llama3-70b-8192", api_key=chat_groq_api)
156
+
157
+
158
+ # --- Initialize Local Models (Embeddings and Judge LLM) ---
159
+ logging.info("Loading local models. This may take a while...")
160
+
161
+ # Embedding Model
162
+ class LocalEmbeddings:
163
+ def __init__(self, model_name=EMBEDDING_MODEL_NAME):
164
+ self.model = SentenceTransformer(model_name)
165
+ def embed_query(self, text):
166
+ return self.model.encode(text).tolist()
167
+ def embed_documents(self, documents):
168
+ return self.model.encode(documents).tolist()
169
+
170
+ embeddings = LocalEmbeddings()
171
+
172
+ # Judge LLM (with quantization for lower memory usage)
173
+ bnb_config = BitsAndBytesConfig(
174
+ load_in_4bit=True,
175
+ bnb_4bit_compute_dtype=torch.float16,
176
+ bnb_4bit_use_double_quant=True,
177
+ bnb_4bit_quant_type="nf4"
178
+ )
179
+
180
+ # use_auth_token is deprecated, token is now passed via login()
181
+ mistral_tokenizer = AutoTokenizer.from_pretrained(JUDGE_MODEL_NAME)
182
+ judge_llm_model = AutoModelForCausalLM.from_pretrained(
183
+ JUDGE_MODEL_NAME,
184
+ quantization_config=bnb_config,
185
+ torch_dtype=torch.float16,
186
+ device_map="auto"
187
+ )
188
+
189
+ judge_pipeline = pipeline(
190
+ "text-generation",
191
+ model=judge_llm_model,
192
+ tokenizer=mistral_tokenizer,
193
+ max_new_tokens=512,
194
+ temperature=0.2,
195
+ top_p=0.95,
196
+ do_sample=True,
197
+ repetition_penalty=1.15,
198
+ )
199
+ logging.info("✅ All models and clients are ready.")
200
+
201
+
202
+ # ==============================================================================
203
+ # 4. CORE APPLICATION LOGIC AND FUNCTIONS
204
+ # ==============================================================================
205
+
206
+ # --- The rest of your functions go here, unchanged. ---
207
+ # e.g., EvaluationScore, CohereReranker, load_data_from_json,
208
+ # store_data_to_qdrant, find_similar_roles, etc.
209
+ # ... (All your other functions from the original script) ...
210
+ # I will include them for completeness.
211
+
212
+ class EvaluationScore(str, Enum):
213
+ POOR = "Poor"
214
+ MEDIUM = "Medium"
215
+ GOOD = "Good"
216
+ EXCELLENT = "Excellent"
217
+
218
+ class CohereReranker:
219
+ def __init__(self, client):
220
+ self.client = client
221
+ def compress_documents(self, documents, query):
222
+ # ... function code ...
223
+ pass
224
+
225
+ reranker = CohereReranker(cohere_client)
226
+
227
+ def load_data_from_json(file_path):
228
+ # ... function code ...
229
+ pass
230
+
231
+ def verify_qdrant_collection(collection_name=QDRANT_COLLECTION_NAME):
232
+ # ... function code ...
233
+ pass
234
+
235
+ def store_data_to_qdrant(data, collection_name=QDRANT_COLLECTION_NAME, batch_size=100):
236
+ # ... function code ...
237
+ pass
238
+
239
+ def find_similar_roles(user_role, all_roles, top_k=3):
240
+ # ... function code ...
241
+ pass
242
+
243
+ def get_role_questions(job_role):
244
+ # ... function code ...
245
+ pass
246
+
247
+ def retrieve_interview_data(job_role, all_roles):
248
+ # ... function code ...
249
+ pass
250
+
251
+ def random_context_chunks(retrieved_data, k=3):
252
+ # ... function code ...
253
+ pass
254
+
255
+ def eval_question_quality(question: str, job_role: str, seniority: str, judge_pipeline=judge_pipeline):
256
+ # ... function code ...
257
+ pass
258
+
259
+ def generate_reference_answer(question, job_role, seniority):
260
+ # ... function code ...
261
+ pass
262
+
263
+ def evaluate_answer(question: str, answer: str, ref_answer: str, job_role: str, seniority: str, judge_pipeline=judge_pipeline):
264
+ # ... function code ...
265
+ pass
266
+
267
+ def build_interview_prompt(conversation_history, user_response, context, job_role, skills, seniority, difficulty_adjustment=None):
268
+ # ... function code ...
269
+ pass
270
+
271
+ def generate_llm_interview_report(interview_state, job_role, seniority):
272
+ # ... function code ...
273
+ pass
274
+
275
+ def extract_candidate_details(file_path):
276
+ # ... function code ...
277
+ pass
278
+
279
+ def extract_job_details(job_description):
280
+ # ... function code ...
281
+ pass
282
+
283
+ def extract_all_roles_from_qdrant(collection_name=QDRANT_COLLECTION_NAME):
284
+ # ... function code ...
285
+ pass
286
+
287
+
288
+ # Example of how to run (for testing purposes)
289
+ if __name__ == '__main__':
290
+ logging.info("Starting a test run...")
291
+ try:
292
+ all_roles = extract_all_roles_from_qdrant()
293
+ if not all_roles:
294
+ logging.warning("No roles found in Qdrant. Using a default list for testing.")
295
+ all_roles = ['data scientist', 'machine learning engineer', 'software engineer']
296
+
297
+ job_role = "ml engineer" # intentionally misspelled
298
+ qa_pairs = retrieve_interview_data(job_role, all_roles)
299
+
300
+ if qa_pairs:
301
+ logging.info(f"Successfully retrieved {len(qa_pairs)} QA pairs for role '{job_role}'.")
302
+ # print("First QA pair:", qa_pairs[0])
303
+ else:
304
+ logging.error(f"Could not retrieve any QA pairs for role '{job_role}'.")
305
+
306
+ except Exception as e:
307
+ logging.critical(f"A critical error occurred during the test run: {e}", exc_info=True)
requirements.txt ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core ML/AI
2
+ transformers
3
+ sentence-transformers
4
+ bitsandbytes
5
+ accelerate
6
+
7
+ # Bark TTS (latest from GitHub)
8
+ git+https://github.com/suno-ai/bark.git
9
+
10
+ # OpenAI Whisper (latest from GitHub)
11
+ git+https://github.com/openai/whisper.git
12
+
13
+ # Audio
14
+ soundfile
15
+ sounddevice
16
+ pyaudio
17
+ ffmpeg-python
18
+
19
+ # TTS
20
+ TTS
21
+ gtts
22
+
23
+ # STT
24
+ whisper
25
+
26
+ # NLP & LLM Tools
27
+ langchain
28
+ langchain_community
29
+ langchain_groq
30
+ langchain_huggingface
31
+ llama-index
32
+ cohere
33
+
34
+ # Vector DB
35
+ qdrant_client
36
+
37
+ # UI
38
+ gradio
39
+
40
+ # File Parsing & Input
41
+ textract
42
+ PyPDF2
43
+ python-docx
44
+
45
+ # Utility
46
+ inputimeout
47
+ fuzzywuzzy
48
+ numpy==1.24
49
+ opencv-python==4.7.0.72
50
+ pip==23.3.1