husseinelsaadi commited on
Commit
cf6b4d0
·
verified ·
1 Parent(s): 1d6f28e

Add working app.py

Browse files
Files changed (2) hide show
  1. app.py +1858 -0
  2. requirements.txt +53 -0
app.py ADDED
@@ -0,0 +1,1858 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import os
3
+ import json
4
+ from langchain_groq import ChatGroq
5
+ from langchain_community.embeddings import HuggingFaceEmbeddings
6
+ from langchain_community.vectorstores import Qdrant
7
+ from langchain.prompts import PromptTemplate
8
+ from langchain.chains import LLMChain
9
+ from langchain.retrievers import ContextualCompressionRetriever
10
+ from langchain.retrievers.document_compressors import CohereRerank
11
+ from qdrant_client import QdrantClient
12
+ import cohere
13
+ import json
14
+ import re
15
+ import time
16
+ from collections import defaultdict
17
+
18
+
19
+ from qdrant_client.http import models
20
+ from qdrant_client.models import PointStruct
21
+ from sklearn.feature_extraction.text import TfidfVectorizer
22
+ from sklearn.neighbors import NearestNeighbors
23
+ from transformers import AutoTokenizer
24
+ from langchain_huggingface import HuggingFaceEndpoint
25
+ from langchain_community.embeddings import HuggingFaceEmbeddings
26
+ import numpy as np
27
+ import os
28
+ from dotenv import load_dotenv
29
+ from enum import Enum
30
+ import time
31
+ from inputimeout import inputimeout, TimeoutOccurred
32
+
33
+
34
+ # Import Qdrant client and models (adjust based on your environment)
35
+ from qdrant_client import QdrantClient
36
+ from qdrant_client.http.models import VectorParams, Distance, Filter, FieldCondition, MatchValue
37
+ from qdrant_client.http.models import PointStruct, Filter, FieldCondition, MatchValue, SearchRequest
38
+ import traceback
39
+ from transformers import pipeline
40
+
41
+ from textwrap import dedent
42
+ import json
43
+ import logging
44
+
45
+ from transformers import pipeline
46
+
47
+
48
+ from dotenv import load_dotenv
49
+ import os
50
+
51
+ load_dotenv() # Load from .env file
52
+
53
+ cohere_api_key = os.getenv("COHERE_API_KEY")
54
+ chat_groq_api = os.getenv("GROQ_API_KEY")
55
+ hf_api_key = os.getenv("HF_API_KEY")
56
+ qdrant_api = os.getenv("QDRANT_API_KEY")
57
+ qdrant_url = os.getenv("QDRANT_API_URL")
58
+
59
+ print("GROQ API Key:", chat_groq_api)
60
+ print("QDRANT API Key:", qdrant_api)
61
+ print("QDRANT API URL:", qdrant_url)
62
+ print("Cohere API Key:", cohere_api_key)
63
+
64
+ from qdrant_client import QdrantClient
65
+
66
+ qdrant_client = QdrantClient(
67
+ url="https://313b1ceb-057f-4b7b-89f5-7b19a213fe65.us-east-1-0.aws.cloud.qdrant.io:6333",
68
+ api_key="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIn0.w13SPZbljbSvt9Ch_0r034QhMFlmEr4ctXqLo2zhxm4",
69
+ )
70
+
71
+ print(qdrant_client.get_collections())
72
+
73
+ class ChatGroq:
74
+ def __init__(self, temperature, model_name, api_key):
75
+ self.temperature = temperature
76
+ self.model_name = model_name
77
+ self.api_key = api_key
78
+ self.api_url = "https://api.groq.com/openai/v1/chat/completions"
79
+
80
+ def predict(self, prompt):
81
+ """Send a request to the Groq API and return the generated response."""
82
+ try:
83
+ headers = {
84
+ "Authorization": f"Bearer {self.api_key}",
85
+ "Content-Type": "application/json"
86
+ }
87
+
88
+ payload = {
89
+ "model": self.model_name,
90
+ "messages": [{"role": "system", "content": "You are an AI interviewer."},
91
+ {"role": "user", "content": prompt}],
92
+ "temperature": self.temperature,
93
+ "max_tokens": 150
94
+ }
95
+
96
+ response = requests.post(self.api_url, headers=headers, json=payload, timeout=10)
97
+ response.raise_for_status() # Raise an error for HTTP codes 4xx/5xx
98
+
99
+ data = response.json()
100
+
101
+ # Extract response text based on Groq API response format
102
+ if "choices" in data and len(data["choices"]) > 0:
103
+ return data["choices"][0]["message"]["content"].strip()
104
+
105
+ logging.warning("Unexpected response structure from Groq API")
106
+ return "Interviewer: Could you tell me more about your relevant experience?"
107
+
108
+ except requests.exceptions.RequestException as e:
109
+ logging.error(f"ChatGroq API error: {e}")
110
+ return "Interviewer: Due to a system issue, let's move on to another question."
111
+ groq_llm = ChatGroq(
112
+ temperature=0.7,
113
+ model_name="llama-3.3-70b-versatile",
114
+ api_key=chat_groq_api
115
+ )
116
+
117
+ from transformers import AutoTokenizer
118
+ #Original Model Name
119
+ MODEL_NAME = "mistralai/Mistral-7B-Instruct-v0.3"
120
+
121
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
122
+
123
+
124
+ from huggingface_hub import login
125
+ import os
126
+
127
+ HF_TOKEN = os.getenv("HF_TOKEN")
128
+
129
+ if HF_TOKEN:
130
+ login(HF_TOKEN)
131
+ else:
132
+ raise EnvironmentError("Missing HF_TOKEN environment variable.")
133
+
134
+
135
+ from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
136
+ import torch
137
+
138
+ MODEL_PATH = "mistralai/Mistral-7B-Instruct-v0.3"
139
+
140
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)
141
+
142
+ model = AutoModelForCausalLM.from_pretrained(
143
+ MODEL_PATH,
144
+ torch_dtype=torch.bfloat16 if torch.backends.mps.is_available() else torch.float32,
145
+ device_map="auto"
146
+ )
147
+
148
+ falcon_pipeline = pipeline(
149
+ "text-generation",
150
+ model=model,
151
+ tokenizer=tokenizer,
152
+ max_new_tokens=128,
153
+ temperature=0.3,
154
+ top_p=0.9,
155
+ do_sample=True,
156
+ repetition_penalty=1.1,
157
+ )
158
+
159
+ # ✅ Test it
160
+ # result = falcon_pipeline("Explain LLMs:")
161
+ # print(result[0]["generated_text"])
162
+
163
+ # embedding model
164
+ from sentence_transformers import SentenceTransformer
165
+
166
+ class LocalEmbeddings:
167
+ def __init__(self, model_name="all-MiniLM-L6-v2"):
168
+ self.model = SentenceTransformer(model_name)
169
+
170
+ def embed_query(self, text):
171
+ return self.model.encode(text).tolist()
172
+
173
+ def embed_documents(self, documents):
174
+ return self.model.encode(documents).tolist()
175
+
176
+
177
+ embeddings = LocalEmbeddings()
178
+
179
+ # import cohere
180
+ qdrant_client = QdrantClient(url=qdrant_url, api_key=qdrant_api,check_compatibility=False)
181
+ co = cohere.Client(api_key=cohere_api_key)
182
+
183
+ class EvaluationScore(str, Enum):
184
+ POOR = "Poor"
185
+ MEDIUM = "Medium"
186
+ GOOD = "Good"
187
+ EXCELLENT = "Excellent"
188
+
189
+ # Cohere Reranker
190
+ class CohereReranker:
191
+ def __init__(self, client):
192
+ self.client = client
193
+
194
+ def compress_documents(self, documents, query):
195
+ if not documents:
196
+ return []
197
+ doc_texts = [doc.page_content for doc in documents]
198
+ try:
199
+ reranked = self.client.rerank(
200
+ query=query,
201
+ documents=doc_texts,
202
+ model="rerank-english-v2.0",
203
+ top_n=5
204
+ )
205
+ return [documents[result.index] for result in reranked.results]
206
+ except Exception as e:
207
+ logging.error(f"Error in CohereReranker.compress_documents: {e}")
208
+ return documents[:5]
209
+
210
+ reranker = CohereReranker(co)
211
+
212
+ def load_data_from_json(file_path):
213
+ """Load interview Q&A data from a JSON file."""
214
+ try:
215
+ with open(file_path, "r", encoding="utf-8") as f:
216
+ data = json.load(f)
217
+ job_role_buckets = defaultdict(list)
218
+ for idx, item in enumerate(data):
219
+ try:
220
+ job_role = item["Job Role"].lower().strip()
221
+ question = item["Questions"].strip()
222
+ answer = item["Answers"].strip()
223
+ job_role_buckets[job_role].append({"question": question, "answer": answer})
224
+ except KeyError as e:
225
+ logging.warning(f"Skipping item {idx}: missing key {e}")
226
+ return job_role_buckets # <--- You missed this!
227
+ except Exception as e:
228
+ logging.error(f"Error loading data: {e}")
229
+ raise
230
+
231
+
232
+ def verify_qdrant_collection(collection_name='interview_questions'):
233
+ """Verify if a Qdrant collection exists with the correct configuration."""
234
+ try:
235
+ collection_info = qdrant_client.get_collection(collection_name)
236
+ vector_size = collection_info.config.params.vectors.size
237
+ logging.info(f"Collection '{collection_name}' exists with vector size: {vector_size}")
238
+ return True
239
+ except Exception as e:
240
+ logging.warning(f"Collection '{collection_name}' not found: {e}")
241
+ return False
242
+
243
+
244
+
245
+
246
+ def store_data_to_qdrant(data, collection_name='interview_questions', batch_size=100):
247
+ """Store interview data in the Qdrant vector database."""
248
+ try:
249
+ # Check if collection exists, otherwise create it
250
+ if not verify_qdrant_collection(collection_name):
251
+ try:
252
+ qdrant_client.create_collection(
253
+ collection_name=collection_name,
254
+ vectors_config=VectorParams(size=384, distance=Distance.COSINE)
255
+ )
256
+ logging.info(f"Created collection '{collection_name}'")
257
+ except Exception as e:
258
+ logging.error(f"Error creating collection: {e}\n{traceback.format_exc()}")
259
+ return False
260
+
261
+ points = []
262
+ point_id = 0
263
+ total_points = sum(len(qa_list) for qa_list in data.values())
264
+ processed = 0
265
+
266
+ for job_role, qa_list in data.items():
267
+ for entry in qa_list:
268
+ try:
269
+ emb = embeddings.embed_query(entry["question"])
270
+ print(f"Embedding shape: {len(emb)}")
271
+
272
+ if not emb or len(emb) != 384:
273
+ logging.warning(f"Skipping point {point_id} due to invalid embedding length: {len(emb)}")
274
+ continue
275
+
276
+ points.append(PointStruct(
277
+ id=point_id,
278
+ vector=emb,
279
+ payload={
280
+ "job_role": job_role,
281
+ "question": entry["question"],
282
+ "answer": entry["answer"]
283
+ }
284
+ ))
285
+ point_id += 1
286
+ processed += 1
287
+
288
+ # Batch upload
289
+ if len(points) >= batch_size:
290
+ try:
291
+ qdrant_client.upsert(collection_name=collection_name, points=points)
292
+ logging.info(f"Stored {processed}/{total_points} points ({processed/total_points*100:.1f}%)")
293
+ except Exception as upsert_err:
294
+ logging.error(f"Error during upsert: {upsert_err}\n{traceback.format_exc()}")
295
+ points = []
296
+
297
+ except Exception as embed_err:
298
+ logging.error(f"Embedding error for point {point_id}: {embed_err}\n{traceback.format_exc()}")
299
+
300
+ # Final batch upload
301
+ if points:
302
+ try:
303
+ qdrant_client.upsert(collection_name=collection_name, points=points)
304
+ logging.info(f"Stored final batch of {len(points)} points")
305
+ except Exception as final_upsert_err:
306
+ logging.error(f"Error during final upsert: {final_upsert_err}\n{traceback.format_exc()}")
307
+
308
+ # Final verification
309
+ try:
310
+ count = qdrant_client.count(collection_name=collection_name, exact=True).count
311
+ print("Current count:", count)
312
+ logging.info(f"✅ Successfully stored {count} points in Qdrant")
313
+ if count != total_points:
314
+ logging.warning(f"Expected {total_points} points but stored {count}")
315
+ except Exception as count_err:
316
+ logging.error(f"Error verifying stored points: {count_err}\n{traceback.format_exc()}")
317
+
318
+ return True
319
+
320
+ except Exception as e:
321
+ logging.error(f"Error storing data to Qdrant: {e}\n{traceback.format_exc()}")
322
+ return False
323
+
324
+ # to ensure cosine similarity use
325
+ info = qdrant_client.get_collection("interview_questions")
326
+ print(info.config.params.vectors.distance)
327
+
328
+ def extract_all_roles_from_qdrant(collection_name='interview_questions'):
329
+ """ Extract all unique job roles from the Qdrant vector store """
330
+ try:
331
+ all_roles = set()
332
+ scroll_offset = None
333
+
334
+ while True:
335
+ response = qdrant_client.scroll(
336
+ collection_name=collection_name,
337
+ limit=200,
338
+ offset=scroll_offset,
339
+ with_payload=True
340
+ )
341
+ points, next_page_offset = response
342
+
343
+ if not points:
344
+ break
345
+
346
+ for point in points:
347
+ role = point.payload.get("job_role", "").strip().lower()
348
+ if role:
349
+ all_roles.add(role)
350
+
351
+ if not next_page_offset:
352
+ break
353
+
354
+ scroll_offset = next_page_offset
355
+
356
+ if not all_roles:
357
+ logging.warning("[Qdrant] No roles found in payloads.")
358
+ else:
359
+ logging.info(f"[Qdrant] Extracted {len(all_roles)} unique job roles.")
360
+
361
+ return list(all_roles)
362
+ except Exception as e:
363
+ logging.error(f"Error extracting roles from Qdrant: {e}")
364
+ return []
365
+
366
+ import numpy as np
367
+ import logging
368
+ from sklearn.metrics.pairwise import cosine_similarity
369
+
370
+ def find_similar_roles(user_role, all_roles, top_k=3):
371
+ """
372
+ Find the most similar job roles to the given user_role using embeddings.
373
+ """
374
+ try:
375
+ # Clean inputs
376
+ user_role = user_role.strip().lower()
377
+ if not user_role or not all_roles or not isinstance(all_roles, list):
378
+ logging.warning("Invalid input for role similarity")
379
+ return []
380
+
381
+ # Embed user role
382
+ try:
383
+ user_embedding = embeddings.embed_query(user_role)
384
+ if user_embedding is None:
385
+ logging.error("User embedding is None")
386
+ return []
387
+ except Exception as e:
388
+ logging.error(f"Error embedding user role: {type(e).__name__}: {e}")
389
+ return []
390
+
391
+ # Embed all roles
392
+ try:
393
+ role_embeddings = []
394
+ valid_roles = []
395
+ for role in all_roles:
396
+ emb = embeddings.embed_query(role.lower())
397
+ if emb is not None:
398
+ role_embeddings.append(emb)
399
+ valid_roles.append(role)
400
+ else:
401
+ logging.warning(f"Skipping role with no embedding: {role}")
402
+ except Exception as e:
403
+ logging.error(f"Error embedding all roles: {type(e).__name__}: {e}")
404
+ return []
405
+
406
+ if not role_embeddings:
407
+ logging.error("All role embeddings failed")
408
+ return []
409
+
410
+ # Compute similarities
411
+ similarities = cosine_similarity([user_embedding], role_embeddings)[0]
412
+ top_indices = np.argsort(similarities)[::-1][:top_k]
413
+
414
+ similar_roles = [valid_roles[i] for i in top_indices]
415
+ logging.debug(f"Similar roles to '{user_role}': {similar_roles}")
416
+ return similar_roles
417
+
418
+ except Exception as e:
419
+ logging.error(f"Error finding similar roles: {type(e).__name__}: {e}", exc_info=True)
420
+ return []
421
+
422
+ # RETREIVE ALL DATA RELATED TO THE JOB ROLE NOT JUST TOP_K
423
+ def get_role_questions(job_role):
424
+ try:
425
+ if not job_role:
426
+ logging.warning("Job role is empty.")
427
+ return []
428
+
429
+ filter_by_role = Filter(
430
+ must=[FieldCondition(
431
+ key="job_role",
432
+ match=MatchValue(value=job_role.lower())
433
+ )]
434
+ )
435
+
436
+ all_results = []
437
+ offset = None
438
+ while True:
439
+ results, next_page_offset = qdrant_client.scroll(
440
+ collection_name="interview_questions",
441
+ scroll_filter=filter_by_role,
442
+ with_payload=True,
443
+ with_vectors=False,
444
+ limit=100, # batch size
445
+ offset=offset
446
+ )
447
+ all_results.extend(results)
448
+
449
+ if not next_page_offset:
450
+ break
451
+ offset = next_page_offset
452
+
453
+ parsed_results = [{
454
+ "question": r.payload.get("question"),
455
+ "answer": r.payload.get("answer"),
456
+ "job_role": r.payload.get("job_role")
457
+ } for r in all_results]
458
+
459
+ return parsed_results
460
+
461
+ except Exception as e:
462
+ logging.error(f"Error fetching role questions: {type(e).__name__}: {e}", exc_info=True)
463
+ return []
464
+
465
+ def retrieve_interview_data(job_role, all_roles):
466
+ """
467
+ Retrieve all interview Q&A for a given job role.
468
+ Falls back to similar roles if no data found.
469
+
470
+ Args:
471
+ job_role (str): Input job role (can be misspelled)
472
+ all_roles (list): Full list of available job roles
473
+
474
+ Returns:
475
+ list: List of QA dicts with keys: 'question', 'answer', 'job_role'
476
+ """
477
+ import logging
478
+ logging.basicConfig(level=logging.INFO)
479
+
480
+ job_role = job_role.strip().lower()
481
+ seen_questions = set()
482
+ final_results = []
483
+
484
+ # Step 1: Try exact match (fetch all questions for role)
485
+ logging.info(f"Trying to fetch all data for exact role: '{job_role}'")
486
+ exact_matches = get_role_questions(job_role)
487
+
488
+ for qa in exact_matches:
489
+ question = qa["question"]
490
+ if question and question not in seen_questions:
491
+ seen_questions.add(question)
492
+ final_results.append(qa)
493
+
494
+ if final_results:
495
+ logging.info(f"Found {len(final_results)} QA pairs for exact role '{job_role}'")
496
+ return final_results
497
+
498
+ logging.warning(f"No data found for role '{job_role}'. Trying similar roles...")
499
+
500
+ # Step 2: No matches — find similar roles
501
+ similar_roles = find_similar_roles(job_role, all_roles, top_k=3)
502
+
503
+ if not similar_roles:
504
+ logging.warning("No similar roles found.")
505
+ return []
506
+
507
+ logging.info(f"Found similar roles: {similar_roles}")
508
+
509
+ # Step 3: Retrieve data for each similar role (all questions)
510
+ for role in similar_roles:
511
+ logging.info(f"Fetching data for similar role: '{role}'")
512
+ role_qa = get_role_questions(role)
513
+
514
+ for qa in role_qa:
515
+ question = qa["question"]
516
+ if question and question not in seen_questions:
517
+ seen_questions.add(question)
518
+ final_results.append(qa)
519
+
520
+ logging.info(f"Retrieved total {len(final_results)} QA pairs from similar roles")
521
+ return final_results
522
+
523
+ import random
524
+
525
+ def random_context_chunks(retrieved_data, k=3):
526
+ chunks = random.sample(retrieved_data, k)
527
+ return "\n\n".join([f"Q: {item['question']}\nA: {item['answer']}" for item in chunks])
528
+
529
+ import json
530
+ import logging
531
+ import re
532
+ from typing import Dict
533
+
534
+ def eval_question_quality(
535
+ question: str,
536
+ job_role: str,
537
+ seniority: str,
538
+ judge_pipeline=None,
539
+ max_retries=1 # Allow at least 1 retry on parse fail
540
+ ) -> Dict[str, str]:
541
+ import time
542
+ try:
543
+ # Use provided pipeline or fall back to global
544
+ if judge_pipeline is None:
545
+ judge_pipeline = globals().get("judge_pipeline")
546
+
547
+ if not judge_pipeline:
548
+ return {
549
+ "Score": "Error",
550
+ "Reasoning": "Judge pipeline not available",
551
+ "Improvements": "Please provide a valid language model pipeline"
552
+ }
553
+
554
+ prompt = f"""
555
+ ... (same as your prompt) ...
556
+ Now evaluate this question:
557
+ \"{question}\"
558
+ """
559
+
560
+ for attempt in range(max_retries + 1):
561
+ response = judge_pipeline(
562
+ prompt,
563
+ max_new_tokens=512,
564
+ do_sample=False,
565
+ temperature=0.1,
566
+ repetition_penalty=1.2
567
+ )[0]["generated_text"]
568
+
569
+ try:
570
+ # Fallback to last {...} block
571
+ match = re.search(r'\{.*\}', response, re.DOTALL)
572
+ if not match:
573
+ raise ValueError("Could not locate JSON structure in model output.")
574
+ json_str = match.group(0)
575
+ result = json.loads(json_str)
576
+
577
+ # Validate required fields and values
578
+ required_keys = ["Score", "Reasoning", "Improvements"]
579
+ valid_scores = {"Poor", "Medium", "Good", "Excellent"}
580
+ if not all(k in result for k in required_keys):
581
+ raise ValueError("Missing required fields.")
582
+ if result["Score"] not in valid_scores:
583
+ raise ValueError("Invalid score value.")
584
+ return result
585
+
586
+ except Exception as e:
587
+ logging.warning(f"Attempt {attempt+1} JSON parsing failed: {e}")
588
+ time.sleep(0.2) # Small delay before retry
589
+
590
+ # If all attempts fail, return a default valid dict
591
+ return {
592
+ "Score": "Poor",
593
+ "Reasoning": "The evaluation model failed to produce a valid score, so defaulted to 'Poor'. Check model output and prompt formatting.",
594
+ "Improvements": [
595
+ "Ensure the question is clear and role-relevant.",
596
+ "Double-check prompt and formatting.",
597
+ "Try rephrasing the question to match rubric."
598
+ ]
599
+ }
600
+
601
+ except Exception as e:
602
+ logging.error(f"Error in eval_question_quality: {type(e)._name_}: {e}", exc_info=True)
603
+ return {
604
+ "Score": "Poor",
605
+ "Reasoning": f"Critical error occurred: {str(e)}. Defaulted to 'Poor'.",
606
+ "Improvements": [
607
+ "Retry with a different question.",
608
+ "Check your judge pipeline connection.",
609
+ "Contact support if this persists."
610
+ ]
611
+ }
612
+
613
+ def evaluate_answer(
614
+ question: str,
615
+ answer: str,
616
+ ref_answer: str,
617
+ job_role: str,
618
+ seniority: str,
619
+ judge_pipeline=None,
620
+ max_retries=1
621
+ ) -> Dict[str, str]:
622
+ """
623
+ Evaluates a candidate's answer to an interview question and returns a structured judgment.
624
+ Guarantees a valid, actionable result even if the model fails.
625
+ """
626
+
627
+ import time
628
+ try:
629
+ if judge_pipeline is None:
630
+ judge_pipeline = globals().get("judge_pipeline")
631
+
632
+ if not judge_pipeline:
633
+ return {
634
+ "Score": "Error",
635
+ "Reasoning": "Judge pipeline not available",
636
+ "Improvements": [
637
+ "Please provide a valid language model pipeline"
638
+ ]
639
+ }
640
+
641
+ # Enhanced prompt (your version)
642
+ prompt = f"""
643
+ You are an expert technical interviewer evaluating a candidate's response for a {job_role} position at the {seniority} level.
644
+
645
+ You are provided with:
646
+ - The question asked
647
+ - The candidate's response
648
+ - A reference answer that represents a high-quality expected answer
649
+
650
+ Evaluate the candidate's response based on:
651
+ - Technical correctness
652
+ - Clarity and depth of explanation
653
+ - Relevance to the job role and seniority
654
+ - Completeness and structure
655
+
656
+ Be objective, concise, and use professional language. Be fair but critical.
657
+
658
+ --------------------------
659
+ Question:
660
+ {question}
661
+
662
+ Candidate Answer:
663
+ {answer}
664
+
665
+ Reference Answer:
666
+ {ref_answer}
667
+ --------------------------
668
+
669
+ Now return your evaluation as a valid JSON object using exactly these keys:
670
+ - "Score": One of ["Poor", "Medium", "Good", "Excellent"]
671
+ - "Reasoning": 2-3 sentence explanation justifying the score, covering clarity, accuracy, completeness, or relevance
672
+ - "Improvements": A list of 2-3 specific and constructive suggestions to help the candidate improve this answer
673
+
674
+ Example:
675
+ {{
676
+ "Score": "Good",
677
+ "Reasoning": "The answer demonstrates a good understanding of the concept and touches on key ideas, but lacks depth in explaining the trade-offs between techniques.",
678
+ "Improvements": [
679
+ "Explain when this method might fail or produce biased results",
680
+ "Include examples or metrics to support the explanation",
681
+ "Clarify the specific business impact or outcome achieved"
682
+ ]
683
+ }}
684
+
685
+ Respond only with the JSON:
686
+ """
687
+ for attempt in range(max_retries + 1):
688
+ output = judge_pipeline(
689
+ prompt,
690
+ max_new_tokens=512,
691
+ temperature=0.3,
692
+ do_sample=False
693
+ )[0]["generated_text"]
694
+
695
+ # Try to extract JSON response from output robustly
696
+ try:
697
+ start_idx = output.rfind("{")
698
+ end_idx = output.rfind("}") + 1
699
+
700
+ if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
701
+ json_str = output[start_idx:end_idx]
702
+ result = json.loads(json_str)
703
+ valid_scores = {"Poor", "Medium", "Good", "Excellent"}
704
+ if result.get("Score") in valid_scores:
705
+ return {
706
+ "Score": result["Score"],
707
+ "Reasoning": result.get("Reasoning", "No explanation provided."),
708
+ "Improvements": result.get("Improvements", ["No improvement suggestions provided."])
709
+ }
710
+ else:
711
+ raise ValueError(f"Invalid Score value: {result.get('Score')}")
712
+ else:
713
+ raise ValueError("JSON format not found in output")
714
+ except Exception as e:
715
+ logging.warning(f"evaluate_answer: Attempt {attempt+1} failed to parse model output: {e}")
716
+ time.sleep(0.2) # Small wait before retry
717
+
718
+ # Fallback: always return a default 'Poor' score if all attempts fail
719
+ return {
720
+ "Score": "Poor",
721
+ "Reasoning": "The evaluation model failed to produce a valid score or parse output; defaulted to 'Poor'. Please check model output and prompt formatting.",
722
+ "Improvements": [
723
+ "Be more specific and detailed in the answer.",
724
+ "Structure your response with clear points.",
725
+ "Relate your answer more closely to the job role and question."
726
+ ]
727
+ }
728
+ except Exception as e:
729
+ logging.error(f"Evaluation failed: {e}", exc_info=True)
730
+ return {
731
+ "Score": "Poor",
732
+ "Reasoning": f"Critical error occurred: {str(e)}. Defaulted to 'Poor'.",
733
+ "Improvements": [
734
+ "Try again with a different answer.",
735
+ "Check your judge pipeline connection.",
736
+ "Contact support if the error persists."
737
+ ]
738
+ }
739
+
740
+ # SAME BUT USING LLAMA 3.3 FROM GROQ
741
+ def generate_reference_answer(question, job_role, seniority):
742
+ """
743
+ Generates a high-quality reference answer using Groq-hosted LLaMA model.
744
+
745
+ Args:
746
+ question (str): Interview question to answer.
747
+ job_role (str): Target job role (e.g., "Frontend Developer").
748
+ seniority (str): Experience level (e.g., "Mid-Level").
749
+
750
+ Returns:
751
+ str: Clean, generated reference answer or error message.
752
+ """
753
+ try:
754
+ # Clean, role-specific prompt
755
+ prompt = f"""You are a {seniority} {job_role}.
756
+
757
+ Q: {question}
758
+ A:"""
759
+
760
+ # Use Groq-hosted model to generate the answer
761
+ ref_answer = groq_llm.predict(prompt)
762
+
763
+ if not ref_answer.strip():
764
+ return "Reference answer not generated."
765
+
766
+ return ref_answer.strip()
767
+
768
+ except Exception as e:
769
+ logging.error(f"Error generating reference answer: {e}", exc_info=True)
770
+ return "Unable to generate reference answer due to an error"
771
+
772
+ def interpret_confidence(voice_label, face_label, answer_score_label,k=0.2):
773
+ # Map expressions to rough numerical confidence levels
774
+ emotion_map = {
775
+ "happy": 0.9, "neutral": 0.6, "surprised": 0.7, "sad": 0.4,
776
+ "angry": 0.3, "disgust": 0.2, "fear": 0.3,
777
+ }
778
+
779
+ answer_score_map = {
780
+ "excellent": 1.0,
781
+ "good": 0.8,
782
+ "medium": 0.6,
783
+ "poor": 0.3
784
+ }
785
+
786
+ voice_score = emotion_map.get(voice_label, 0.5)
787
+ face_score = emotion_map.get(face_label, 0.5)
788
+ answer_score = answer_score_map.get(answer_score_label, 0.5)
789
+
790
+ # Adjust weights as needed (emotions may be less reliable than verbal answers)
791
+ avg_emotion = (voice_score + face_score) /2
792
+ control_bonus = max(0,answer_score - avg_emotion) *k
793
+ effective_confidence = (
794
+ 0.5 * answer_score +
795
+ 0.22 * voice_score +
796
+ 0.18 * face_score +
797
+ 0.1 *control_bonus
798
+ )
799
+
800
+ return {
801
+ "effective_confidence": round(effective_confidence, 3),
802
+ "answer_score": round(answer_score, 2),
803
+ "voice_score": round(voice_score, 2),
804
+ "face_score": round(face_score, 2),
805
+ "control_bonus": round(control_bonus, 3)
806
+ }
807
+
808
+ def build_interview_prompt(conversation_history, user_response, context, job_role, skills, seniority,
809
+ difficulty_adjustment=None, voice_label=None, face_label=None, effective_confidence=None):
810
+ """Build a prompt for generating the next interview question with adaptive difficulty and fairness logic."""
811
+
812
+ interview_template = """
813
+ You are an AI interviewer conducting a real-time interview for a {job_role} position.
814
+
815
+ Your objective is to thoroughly evaluate the candidate's suitability for the role using smart, structured, and adaptive questioning.
816
+
817
+ ---
818
+
819
+ Interview Rules and Principles:
820
+ - The **baseline difficulty** of questions must match the candidate’s seniority level (e.g., junior, mid-level, senior).
821
+ - Use your judgment to increase difficulty **slightly** if the candidate performs well, or simplify if they struggle — but never drop below the expected baseline for their level.
822
+ - Avoid asking extremely difficult questions to junior candidates unless they’ve clearly demonstrated advanced knowledge.
823
+ - Be fair: candidates for the same role should be evaluated within a consistent difficulty range.
824
+ - Adapt your line of questioning gradually and logically based on the **overall flow**, not just the last answer.
825
+ - Include real-world problem-solving scenarios to test how the candidate thinks and behaves practically.
826
+ - You must **lead** the interview and make intelligent decisions about what to ask next.
827
+
828
+ ---
829
+
830
+ Context Use:
831
+ {context_instruction}
832
+ Note:
833
+ If no relevant context was retrieved or the previous answer is unclear, you must still generate a thoughtful interview question using your own knowledge. Do not skip generation. Avoid default or fallback responses — always try to generate a meaningful and fair next question.
834
+
835
+
836
+ ---
837
+
838
+ Job Role: {job_role}
839
+ Seniority Level: {seniority}
840
+ Skills Focus: {skills}
841
+ Difficulty Setting: {difficulty} (based on {difficulty_adjustment})
842
+
843
+ ---
844
+
845
+ Recent Conversation History:
846
+ {history}
847
+
848
+ Candidate's Last Response:
849
+ "{user_response}"
850
+
851
+ Evaluation of Last Response:
852
+ {response_evaluation}
853
+
854
+ Voice Tone: {voice_label}
855
+ Facial Expression: {face_label}
856
+ Estimated Confidence Score: {effective_confidence}
857
+
858
+ ---
859
+ ---
860
+
861
+ Important:
862
+ If no relevant context was retrieved or the previous answer is unclear or off-topic,
863
+ you must still generate a meaningful and fair interview question using your own knowledge and best practices.
864
+ Do not skip question generation or fall back to default/filler responses.
865
+
866
+ ---
867
+
868
+ Guidelines for Next Question:
869
+ - If this is the beginning of the interview, start with a question about the candidate’s background or experience.
870
+ - Base the difficulty primarily on the seniority level, with light adjustment from recent performance.
871
+ - Focus on core skills, real-world applications, and depth of reasoning.
872
+ - Ask only one question. Be clear and concise.
873
+
874
+ Generate the next interview question now:
875
+ """
876
+
877
+ # Calculate difficulty phrase
878
+ if difficulty_adjustment == "harder":
879
+ difficulty = f"slightly more challenging than typical for {seniority}"
880
+ elif difficulty_adjustment == "easier":
881
+ difficulty = f"slightly easier than typical for {seniority}"
882
+ else:
883
+ difficulty = f"appropriate for {seniority}"
884
+
885
+ # Choose context logic
886
+ if context.strip():
887
+ context_instruction = (
888
+ "Use both your own expertise and the provided context from relevant interview datasets. "
889
+ "You can either build on questions from the dataset or generate your own."
890
+ )
891
+ context = context.strip()
892
+ else:
893
+ context_instruction = (
894
+ "No specific context retrieved. Use your own knowledge and best practices to craft a question."
895
+ )
896
+ context = "" # Let it be actually empty!
897
+
898
+
899
+ # Format conversation history (last 6 exchanges max)
900
+ recent_history = conversation_history[-6:] if len(conversation_history) > 6 else conversation_history
901
+ formatted_history = "\n".join([f"{msg['role'].capitalize()}: {msg['content']}" for msg in recent_history])
902
+
903
+ # Add evaluation summary if available
904
+
905
+ if conversation_history and conversation_history[-1].get("evaluation"):
906
+ eval_data = conversation_history[-1]["evaluation"][-1]
907
+ response_evaluation = f"""
908
+ - Score: {eval_data.get('Score', 'N/A')}
909
+ - Reasoning: {eval_data.get('Reasoning', 'N/A')}
910
+ - Improvements: {eval_data.get('Improvements', 'N/A')}
911
+ """
912
+ else:
913
+ response_evaluation = "No evaluation available yet."
914
+
915
+
916
+ # Fill the template
917
+ prompt = interview_template.format(
918
+ job_role=job_role,
919
+ seniority=seniority,
920
+ skills=skills,
921
+ difficulty=difficulty,
922
+ difficulty_adjustment=difficulty_adjustment if difficulty_adjustment else "default seniority",
923
+ context_instruction=context_instruction,
924
+ context=context,
925
+ history=formatted_history,
926
+ user_response=user_response,
927
+ response_evaluation=response_evaluation.strip(),
928
+ voice_label=voice_label or "unknown",
929
+ face_label=face_label or "unknown",
930
+ effective_confidence=effective_confidence if effective_confidence is not None else "N/A"
931
+ )
932
+
933
+ return prompt
934
+
935
+
936
+ def generate_llm_interview_report(
937
+ interview_state, logged_samples, job_role, seniority
938
+ ):
939
+ from collections import Counter
940
+
941
+ # Helper for converting score to 1–5
942
+ def score_label(label):
943
+ mapping = {
944
+ "confident": 5, "calm": 4, "neutral": 3, "nervous": 2, "anxious": 1, "unknown": 3
945
+ }
946
+ return mapping.get(label.lower(), 3)
947
+
948
+ def section_score(vals):
949
+ return round(sum(vals)/len(vals), 2) if vals else "N/A"
950
+
951
+ # Aggregate info
952
+ scores, voice_conf, face_conf, comm_scores = [], [], [], []
953
+ tech_details, comm_details, emotion_details, relevance_details, problem_details = [], [], [], [], []
954
+
955
+ for entry in logged_samples:
956
+ answer_eval = entry.get("answer_evaluation", {})
957
+ score = answer_eval.get("Score", "Not Evaluated")
958
+ reasoning = answer_eval.get("Reasoning", "")
959
+ if score.lower() in ["excellent", "good", "medium", "poor"]:
960
+ score_map = {"excellent": 5, "good": 4, "medium": 3, "poor": 2}
961
+ scores.append(score_map[score.lower()])
962
+ # Section details
963
+ tech_details.append(reasoning)
964
+ comm_details.append(reasoning)
965
+ # Emotions/confidence
966
+ voice_conf.append(score_label(entry.get("voice_label", "unknown")))
967
+ face_conf.append(score_label(entry.get("face_label", "unknown")))
968
+ # Communication estimate
969
+ if entry["user_answer"]:
970
+ length = len(entry["user_answer"].split())
971
+ comm_score = min(5, max(2, length // 30))
972
+ comm_scores.append(comm_score)
973
+
974
+ # Compute averages for sections
975
+ avg_problem = section_score(scores)
976
+ avg_tech = section_score(scores)
977
+ avg_comm = section_score(comm_scores)
978
+ avg_emotion = section_score([(v+f)/2 for v, f in zip(voice_conf, face_conf)])
979
+
980
+ # Compute decision heuristics
981
+ section_averages = [avg_problem, avg_tech, avg_comm, avg_emotion]
982
+ numeric_avgs = [v for v in section_averages if isinstance(v, (float, int))]
983
+ avg_overall = round(sum(numeric_avgs) / len(numeric_avgs), 2) if numeric_avgs else 0
984
+
985
+ # Hiring logic (you can customize thresholds)
986
+ if avg_overall >= 4.5:
987
+ verdict = "Strong Hire"
988
+ elif avg_overall >= 4.0:
989
+ verdict = "Hire"
990
+ elif avg_overall >= 3.0:
991
+ verdict = "Conditional Hire"
992
+ else:
993
+ verdict = "No Hire"
994
+
995
+ # Build LLM report prompt
996
+ transcript = "\n\n".join([
997
+ f"Q: {e['generated_question']}\nA: {e['user_answer']}\nScore: {e.get('answer_evaluation',{}).get('Score','')}\nReasoning: {e.get('answer_evaluation',{}).get('Reasoning','')}"
998
+ for e in logged_samples
999
+ ])
1000
+
1001
+ prompt = f"""
1002
+ You are a senior technical interviewer at a major tech company.
1003
+
1004
+ Write a structured, realistic hiring report for this {seniority} {job_role} interview, using these section scores (scale 1–5, with 5 best):
1005
+
1006
+ Section-wise Evaluation
1007
+ 1. *Problem Solving & Critical Thinking*: {avg_problem}
1008
+ 2. *Technical Depth & Knowledge*: {avg_tech}
1009
+ 3. *Communication & Clarity*: {avg_comm}
1010
+ 4. *Emotional Composure & Confidence*: {avg_emotion}
1011
+ 5. *Role Relevance*: 5
1012
+
1013
+ *Transcript*
1014
+ {transcript}
1015
+
1016
+ Your report should have the following sections:
1017
+
1018
+ 1. *Executive Summary* (realistic, hiring-committee style)
1019
+ 2. *Section-wise Comments* (for each numbered category above, with short paragraph citing specifics)
1020
+ 3. *Strengths & Weaknesses* (list at least 2 for each)
1021
+ 4. *Final Verdict*: {verdict}
1022
+ 5. *Recommendations* (2–3 for future improvement)
1023
+
1024
+ Use realistic language. If some sections are N/A or lower than others, comment honestly.
1025
+
1026
+ Interview Report:
1027
+ """
1028
+ # LLM call, or just return prompt for review
1029
+ return groq_llm.predict(prompt)
1030
+
1031
+ def get_user_info():
1032
+ """
1033
+ Collects essential information from the candidate before starting the interview.
1034
+ Returns a dictionary with keys: name, job_role, seniority, skills
1035
+ """
1036
+ import logging
1037
+ logging.info("Collecting user information...")
1038
+
1039
+ print("Welcome to the AI Interview Simulator!")
1040
+ print("Let’s set up your mock interview.\n")
1041
+
1042
+ # Get user name
1043
+ name = input("What is your name? ").strip()
1044
+ while not name:
1045
+ print("Please enter your name.")
1046
+ name = input("What is your name? ").strip()
1047
+
1048
+ # Get job role
1049
+ job_role = input(f"Hi {name}, what job role are you preparing for? (e.g. Frontend Developer) ").strip()
1050
+ while not job_role:
1051
+ print("Please specify the job role.")
1052
+ job_role = input("What job role are you preparing for? ").strip()
1053
+
1054
+ # Get seniority level
1055
+ seniority_options = ["Entry-level", "Junior", "Mid-Level", "Senior", "Lead"]
1056
+ print("\nSelect your experience level:")
1057
+ for i, option in enumerate(seniority_options, 1):
1058
+ print(f"{i}. {option}")
1059
+
1060
+ seniority_choice = None
1061
+ while seniority_choice not in range(1, len(seniority_options)+1):
1062
+ try:
1063
+ seniority_choice = int(input("Enter the number corresponding to your level: "))
1064
+ except ValueError:
1065
+ print(f"Please enter a number between 1 and {len(seniority_options)}")
1066
+
1067
+ seniority = seniority_options[seniority_choice - 1]
1068
+
1069
+ # Get skills
1070
+ skills_input = input(f"\nWhat are your top skills relevant to {job_role}? (Separate with commas): ")
1071
+ skills = [skill.strip() for skill in skills_input.split(",") if skill.strip()]
1072
+
1073
+ while not skills:
1074
+ print("Please enter at least one skill.")
1075
+ skills_input = input("Your top skills (comma-separated): ")
1076
+ skills = [skill.strip() for skill in skills_input.split(",") if skill.strip()]
1077
+
1078
+ # Confirm collected info
1079
+ print("\n Interview Setup Complete!")
1080
+ print(f"Name: {name}")
1081
+ print(f"Job Role: {job_role}")
1082
+ print(f"Experience Level: {seniority}")
1083
+ print(f"Skills: {', '.join(skills)}")
1084
+ print("\nStarting your mock interview...\n")
1085
+
1086
+ return {
1087
+ "name": name,
1088
+ "job_role": job_role,
1089
+ "seniority": seniority,
1090
+ "skills": skills
1091
+ }
1092
+
1093
+ import threading
1094
+
1095
+ def wait_for_user_response(timeout=200):
1096
+ """Wait for user input with timeout. Returns '' if no response."""
1097
+ user_input = []
1098
+
1099
+ def get_input():
1100
+ answer = input("Your Answer (within timeout): ").strip()
1101
+ user_input.append(answer)
1102
+
1103
+ thread = threading.Thread(target=get_input)
1104
+ thread.start()
1105
+ thread.join(timeout)
1106
+
1107
+ return user_input[0] if user_input else ""
1108
+
1109
+ import json
1110
+ from datetime import datetime
1111
+ from time import time
1112
+ import random
1113
+
1114
+ def interview_loop(max_questions, timeout_seconds=300, collection_name="interview_questions", judge_pipeline=None, save_path="interview_log.json"):
1115
+
1116
+
1117
+ user_info = get_user_info()
1118
+ job_role = user_info['job_role']
1119
+ seniority = user_info['seniority']
1120
+ skills = user_info['skills']
1121
+
1122
+ all_roles = extract_all_roles_from_qdrant(collection_name=collection_name)
1123
+ retrieved_data = retrieve_interview_data(job_role, all_roles)
1124
+ context_data = random_context_chunks(retrieved_data, k=4)
1125
+
1126
+ conversation_history = []
1127
+ interview_state = {
1128
+ "questions": [],
1129
+ "user_answer": [],
1130
+ "job_role": job_role,
1131
+ "seniority": seniority,
1132
+ "start_time": time()
1133
+ }
1134
+
1135
+ # Store log for evaluation
1136
+ logged_samples = []
1137
+
1138
+ difficulty_adjustment = None
1139
+
1140
+ for i in range(max_questions):
1141
+ last_user_response = conversation_history[-1]['content'] if conversation_history else ""
1142
+
1143
+ # Generate question prompt
1144
+ prompt = build_interview_prompt(
1145
+ conversation_history=conversation_history,
1146
+ user_response=last_user_response,
1147
+ context=context_data,
1148
+ job_role=job_role,
1149
+ skills=skills,
1150
+ seniority=seniority,
1151
+ difficulty_adjustment=difficulty_adjustment
1152
+ )
1153
+ question = groq_llm.predict(prompt)
1154
+ question_eval = eval_question_quality(question, job_role, seniority, judge_pipeline)
1155
+
1156
+ conversation_history.append({'role': "Interviewer", "content": question})
1157
+ print(f"Interviewer: Q{i + 1} : {question}")
1158
+
1159
+ # Wait for user answer
1160
+ start_time = time()
1161
+ user_answer = wait_for_user_response(timeout=timeout_seconds)
1162
+ response_time = time() - start_time
1163
+
1164
+ skipped = False
1165
+ answer_eval = None
1166
+ ref_answer = None
1167
+
1168
+ if not user_answer:
1169
+ print("No Response Received, moving to next question.")
1170
+ user_answer = None
1171
+ skipped = True
1172
+ difficulty_adjustment = "medium"
1173
+ else:
1174
+ conversation_history.append({"role": "Candidate", "content": user_answer})
1175
+
1176
+ ref_answer = generate_reference_answer(question, job_role, seniority)
1177
+ answer_eval = evaluate_answer(
1178
+ question=question,
1179
+ answer=user_answer,
1180
+ ref_answer=ref_answer,
1181
+ job_role=job_role,
1182
+ seniority=seniority,
1183
+ judge_pipeline=judge_pipeline
1184
+ )
1185
+
1186
+
1187
+ interview_state["user_answer"].append(user_answer)
1188
+ # Append inline evaluation for history
1189
+ conversation_history[-1].setdefault('evaluation', []).append({
1190
+ "technical_depth": {
1191
+ "score": answer_eval['Score'],
1192
+ "Reasoning": answer_eval['Reasoning']
1193
+ }
1194
+ })
1195
+
1196
+ # Adjust difficulty
1197
+ score = answer_eval['Score'].lower()
1198
+ if score == "excellent":
1199
+ difficulty_adjustment = "harder"
1200
+ elif score in ['poor', 'medium']:
1201
+ difficulty_adjustment = "easier"
1202
+ else:
1203
+ difficulty_adjustment = None
1204
+
1205
+ # Store for local logging
1206
+ logged_samples.append({
1207
+ "job_role": job_role,
1208
+ "seniority": seniority,
1209
+ "skills": skills,
1210
+ "context": context_data,
1211
+ "prompt": prompt,
1212
+ "generated_question": question,
1213
+ "question_evaluation": question_eval,
1214
+ "user_answer": user_answer,
1215
+ "reference_answer": ref_answer,
1216
+ "answer_evaluation": answer_eval,
1217
+ "skipped": skipped
1218
+ })
1219
+
1220
+ # Store state
1221
+ interview_state['questions'].append({
1222
+ "question": question,
1223
+ "question_evaluation": question_eval,
1224
+ "user_answer": user_answer,
1225
+ "answer_evaluation": answer_eval,
1226
+ "skipped": skipped
1227
+ })
1228
+
1229
+ interview_state['end_time'] = time()
1230
+ report = generate_llm_interview_report(interview_state, job_role, seniority)
1231
+ print("Report : _____________________\n")
1232
+ print(report)
1233
+ print('______________________________________________')
1234
+
1235
+ # Save full interview logs to JSON
1236
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1237
+ filename = f"{save_path.replace('.json', '')}_{timestamp}.json"
1238
+ with open(filename, "w", encoding="utf-8") as f:
1239
+ json.dump(logged_samples, f, indent=2, ensure_ascii=False)
1240
+
1241
+ print(f" Interview log saved to {filename}")
1242
+ print("____________________________________\n")
1243
+
1244
+ print(f"interview state : {interview_state}")
1245
+ return interview_state, report
1246
+
1247
+ from sklearn.metrics import precision_score, recall_score, f1_score
1248
+ import numpy as np
1249
+ # build ground truth for retrieving data for testing
1250
+
1251
+ def build_ground_truth(all_roles):
1252
+ gt = {}
1253
+ for role in all_roles:
1254
+ qa_list = get_role_questions(role)
1255
+ gt[role] = set(q["question"] for q in qa_list if q["question"])
1256
+ return gt
1257
+
1258
+
1259
+ def evaluate_retrieval(job_role, all_roles, k=10):
1260
+ """
1261
+ Evaluate retrieval quality using Precision@k, Recall@k, and F1@k.
1262
+
1263
+ Args:
1264
+ job_role (str): The input job role to search for.
1265
+ all_roles (list): List of all available job roles in the system.
1266
+ k (int): Top-k retrieved questions to evaluate.
1267
+
1268
+ Returns:
1269
+ dict: Evaluation metrics including precision, recall, and f1.
1270
+ """
1271
+
1272
+ # Step 1: Ground Truth (all exact questions stored for this role)
1273
+ ground_truth_qs = set(
1274
+ q["question"].strip()
1275
+ for q in get_role_questions(job_role)
1276
+ if q.get("question")
1277
+ )
1278
+
1279
+ if not ground_truth_qs:
1280
+ print(f"[!] No ground truth found for role: {job_role}")
1281
+ return {}
1282
+
1283
+ # Step 2: Retrieved Questions (may include fallback roles)
1284
+ retrieved_qas = retrieve_interview_data(job_role, all_roles)
1285
+ retrieved_qs = [q["question"].strip() for q in retrieved_qas if q.get("question")]
1286
+
1287
+ # Step 3: Take top-k retrieved (you can also do full if needed)
1288
+ retrieved_top_k = retrieved_qs[:k]
1289
+
1290
+ # Step 4: Binary relevance (1 if in ground truth, 0 if not)
1291
+ y_true = [1 if q in ground_truth_qs else 0 for q in retrieved_top_k]
1292
+ y_pred = [1] * len(y_true) # all retrieved are treated as predicted relevant
1293
+
1294
+ precision = precision_score(y_true, y_pred, zero_division=0)
1295
+ recall = recall_score(y_true, y_pred, zero_division=0)
1296
+ f1 = f1_score(y_true, y_pred, zero_division=0)
1297
+
1298
+ print(f" Retrieval Evaluation for role: '{job_role}' (Top-{k})")
1299
+ print(f"Precision@{k}: {precision:.2f}")
1300
+ print(f"Recall@{k}: {recall:.2f}")
1301
+ print(f"F1@{k}: {f1:.2f}")
1302
+ print(f"Relevant Retrieved: {sum(y_true)}/{len(y_true)}")
1303
+ print("–" * 40)
1304
+
1305
+ return {
1306
+ "job_role": job_role,
1307
+ "precision": precision,
1308
+ "recall": recall,
1309
+ "f1": f1,
1310
+ "relevant_retrieved": sum(y_true),
1311
+ "total_retrieved": len(y_true),
1312
+ "ground_truth_count": len(ground_truth_qs),
1313
+ }
1314
+
1315
+
1316
+ k_values = [5, 10, 20]
1317
+ all_roles = extract_all_roles_from_qdrant(collection_name="interview_questions")
1318
+
1319
+ results = []
1320
+
1321
+ for k in k_values:
1322
+ for role in all_roles:
1323
+ metrics = evaluate_retrieval(role, all_roles, k=k)
1324
+ if metrics: # only if we found ground truth
1325
+ metrics["k"] = k
1326
+ results.append(metrics)
1327
+
1328
+ import pandas as pd
1329
+
1330
+ df = pd.DataFrame(results)
1331
+ summary = df.groupby("k")[["precision", "recall", "f1"]].mean().round(3)
1332
+ print(summary)
1333
+
1334
+ import pandas as pd
1335
+ import matplotlib.pyplot as plt
1336
+ import seaborn as sns
1337
+
1338
+ # Load the dataset
1339
+ df = pd.read_csv("/Users/husseinelsaadi/kaggle-local-project/data/retrieval_metrics_table.csv")
1340
+
1341
+ # Set plot style
1342
+ sns.set(style="whitegrid")
1343
+
1344
+ # Plot 1: Precision per Job Role
1345
+ plt.figure(figsize=(12, 6))
1346
+ sns.barplot(data=df, x="job_role", y="precision")
1347
+ plt.title("Precision@K per Job Role")
1348
+ plt.xticks(rotation=45, ha="right")
1349
+ plt.tight_layout()
1350
+ plt.show()
1351
+
1352
+ # Plot 2: Recall per Job Role
1353
+ plt.figure(figsize=(12, 6))
1354
+ sns.barplot(data=df, x="job_role", y="recall")
1355
+ plt.title("Recall@K per Job Role")
1356
+ plt.xticks(rotation=45, ha="right")
1357
+ plt.tight_layout()
1358
+ plt.show()
1359
+
1360
+ # Plot 3: F1 Score per Job Role
1361
+ plt.figure(figsize=(12, 6))
1362
+ sns.barplot(data=df, x="job_role", y="f1")
1363
+ plt.title("F1@K per Job Role")
1364
+ plt.xticks(rotation=45, ha="right")
1365
+ plt.tight_layout()
1366
+ plt.show()
1367
+
1368
+ # Plot 4: Grouped Bar Chart for Precision, Recall, F1
1369
+ df_melted = df.melt(id_vars="job_role", value_vars=["precision", "recall", "f1"],
1370
+ var_name="Metric", value_name="Score")
1371
+ plt.figure(figsize=(14, 6))
1372
+ sns.barplot(data=df_melted, x="job_role", y="Score", hue="Metric")
1373
+ plt.title("Retrieval Evaluation Metrics per Job Role")
1374
+ plt.xticks(rotation=45, ha="right")
1375
+ plt.legend(title="Metric")
1376
+ plt.tight_layout()
1377
+ plt.show()
1378
+
1379
+ def extract_job_details(job_description):
1380
+ """Extract job details such as title, skills, experience level, and years of experience from the job description."""
1381
+ title_match = re.search(r"(?i)(?:seeking|hiring) a (.+?) to", job_description)
1382
+ job_title = title_match.group(1) if title_match else "Unknown"
1383
+
1384
+ skills_match = re.findall(r"(?i)(?:Proficiency in|Experience with|Knowledge of) (.+?)(?:,|\.| and| or)", job_description)
1385
+ skills = list(set([skill.strip() for skill in skills_match])) if skills_match else []
1386
+
1387
+ experience_match = re.search(r"(\d+)\+? years of experience", job_description)
1388
+ if experience_match:
1389
+ years_experience = int(experience_match.group(1))
1390
+ experience_level = "Senior" if years_experience >= 5 else "Mid" if years_experience >= 3 else "Junior"
1391
+ else:
1392
+ years_experience = None
1393
+ experience_level = "Unknown"
1394
+
1395
+ return {
1396
+ "job_title": job_title,
1397
+ "skills": skills,
1398
+ "experience_level": experience_level,
1399
+ "years_experience": years_experience
1400
+ }
1401
+
1402
+ import re
1403
+ from docx import Document
1404
+ import textract
1405
+ from PyPDF2 import PdfReader
1406
+
1407
+ JOB_TITLES = [
1408
+ "Accountant", "Data Scientist", "Machine Learning Engineer", "Software Engineer",
1409
+ "Developer", "Analyst", "Researcher", "Intern", "Consultant", "Manager",
1410
+ "Engineer", "Specialist", "Project Manager", "Product Manager", "Administrator",
1411
+ "Director", "Officer", "Assistant", "Coordinator", "Supervisor"
1412
+ ]
1413
+
1414
+ def clean_filename_name(filename):
1415
+ # Remove file extension
1416
+ base = re.sub(r"\.[^.]+$", "", filename)
1417
+ base = base.strip()
1418
+
1419
+ # Remove 'cv' or 'CV' words
1420
+ base_clean = re.sub(r"\bcv\b", "", base, flags=re.IGNORECASE).strip()
1421
+
1422
+ # If after removing CV it's empty, return None
1423
+ if not base_clean:
1424
+ return None
1425
+
1426
+ # If it contains any digit, return None (unreliable)
1427
+ if re.search(r"\d", base_clean):
1428
+ return None
1429
+
1430
+ # Replace underscores/dashes with spaces, capitalize
1431
+ base_clean = base_clean.replace("_", " ").replace("-", " ")
1432
+ return base_clean.title()
1433
+
1434
+ def looks_like_job_title(line):
1435
+ for title in JOB_TITLES:
1436
+ pattern = r"\b" + re.escape(title.lower()) + r"\b"
1437
+ if re.search(pattern, line.lower()):
1438
+ return True
1439
+ return False
1440
+
1441
+ def extract_name_from_text(lines):
1442
+ # Try first 3 lines for a name, skipping job titles
1443
+ for i in range(min(1, len(lines))):
1444
+ line = lines[i].strip()
1445
+ if looks_like_job_title(line):
1446
+ return "unknown"
1447
+ if re.search(r"\d", line): # skip lines with digits
1448
+ continue
1449
+ if len(line.split()) > 4 or len(line) > 40: # too long or many words
1450
+ continue
1451
+ # If line has only uppercase words, it's probably not a name
1452
+ if line.isupper():
1453
+ continue
1454
+ # Passed checks, return title-cased line as name
1455
+ return line.title()
1456
+ return None
1457
+
1458
+ def extract_text_from_file(file_path):
1459
+ if file_path.endswith('.pdf'):
1460
+ reader = PdfReader(file_path)
1461
+ text = "\n".join(page.extract_text() or '' for page in reader.pages)
1462
+ elif file_path.endswith('.docx'):
1463
+ doc = Document(file_path)
1464
+ text = "\n".join([para.text for para in doc.paragraphs])
1465
+ else: # For .doc or fallback
1466
+ text = textract.process(file_path).decode('utf-8')
1467
+ return text.strip()
1468
+
1469
+ def extract_candidate_details(file_path):
1470
+ text = extract_text_from_file(file_path)
1471
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
1472
+
1473
+ # Extract name
1474
+ filename = file_path.split("/")[-1] # just filename, no path
1475
+ name = clean_filename_name(filename)
1476
+ if not name:
1477
+ name = extract_name_from_text(lines)
1478
+ if not name:
1479
+ name = "Unknown"
1480
+
1481
+ # Extract skills (basic version)
1482
+ skills = []
1483
+ skills_section = re.search(r"Skills\s*[:\-]?\s*(.+)", text, re.IGNORECASE)
1484
+ if skills_section:
1485
+ raw_skills = skills_section.group(1)
1486
+ skills = [s.strip() for s in re.split(r",|\n|•|-", raw_skills) if s.strip()]
1487
+
1488
+ return {
1489
+ "name": name,
1490
+ "skills": skills
1491
+ }
1492
+
1493
+ import gradio as gr
1494
+ import time
1495
+ import tempfile
1496
+ import numpy as np
1497
+ import scipy.io.wavfile as wavfile
1498
+ import cv2
1499
+ import os
1500
+ import json
1501
+ from moviepy.editor import VideoFileClip
1502
+ import shutil
1503
+ from transformers import BarkModel, AutoProcessor
1504
+ import torch
1505
+ import whisper
1506
+ from transformers import Wav2Vec2Processor, Wav2Vec2ForSequenceClassification
1507
+ import librosa
1508
+
1509
+ # Bark TTS
1510
+ print("🔁 Loading Bark model...")
1511
+ model_bark = BarkModel.from_pretrained("suno/bark")
1512
+ print("✅ Bark model loaded")
1513
+
1514
+ print("🔁 Loading Bark processor...")
1515
+ processor_bark = AutoProcessor.from_pretrained("suno/bark")
1516
+ print("✅ Bark processor loaded")
1517
+ print("🔁 Moving Bark model to device...")
1518
+ model_bark.to("cuda" if torch.cuda.is_available() else "cpu")
1519
+ print("✅ Bark model on device")
1520
+ bark_voice_preset = "v2/en_speaker_6"
1521
+
1522
+ def bark_tts(text):
1523
+ print(f"🔁 Synthesizing TTS for: {text}")
1524
+ inputs = processor_bark(text, return_tensors="pt", voice_preset=bark_voice_preset)
1525
+ inputs = {k: v.to(model_bark.device) for k, v in inputs.items()}
1526
+ speech_values = model_bark.generate(**inputs)
1527
+ speech = speech_values.cpu().numpy().squeeze()
1528
+ speech = (speech * 32767).astype(np.int16)
1529
+ temp_wav = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
1530
+ wavfile.write(temp_wav.name, 22050, speech)
1531
+ return temp_wav.name
1532
+
1533
+ # Whisper STT
1534
+ print("🔁 Loading Whisper model...")
1535
+ whisper_model = whisper.load_model("base")
1536
+ print("✅ Whisper model loaded")
1537
+ def whisper_stt(audio_path):
1538
+ if not audio_path or not os.path.exists(audio_path): return ""
1539
+ result = whisper_model.transcribe(audio_path)
1540
+ return result["text"]
1541
+
1542
+
1543
+ # DeepFace (Video Face Emotion)
1544
+ def ensure_mp4(video_input):
1545
+ # video_input could be a file-like object, a path, or a Gradio temp path
1546
+ if isinstance(video_input, str):
1547
+ input_path = video_input
1548
+ else:
1549
+ # It's a file-like object (rare for Gradio video, but handle it)
1550
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".webm") as temp_in:
1551
+ temp_in.write(video_input.read())
1552
+ input_path = temp_in.name
1553
+
1554
+ # If already mp4, return as is
1555
+ if input_path.endswith(".mp4"):
1556
+ return input_path
1557
+
1558
+ # Convert to mp4 using moviepy
1559
+ mp4_path = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
1560
+ try:
1561
+ clip = VideoFileClip(input_path)
1562
+ clip.write_videofile(mp4_path, codec="libx264", audio=False, verbose=False, logger=None)
1563
+ clip.close()
1564
+ except Exception as e:
1565
+ print("Video conversion failed:", e)
1566
+ # As fallback, just copy original
1567
+ shutil.copy(input_path, mp4_path)
1568
+ return mp4_path
1569
+
1570
+ def analyze_video_emotions(video_input, sample_rate=15):
1571
+ # Convert input to an mp4 file OpenCV can process
1572
+ mp4_path = ensure_mp4(video_input)
1573
+ if not mp4_path or not os.path.exists(mp4_path):
1574
+ return "no_face"
1575
+ cap = cv2.VideoCapture(mp4_path)
1576
+ frame_count = 0
1577
+ emotion_counts = {}
1578
+ while True:
1579
+ ret, frame = cap.read()
1580
+ if not ret: break
1581
+ if frame_count % sample_rate == 0:
1582
+ try:
1583
+ result = DeepFace.analyze(frame, actions=['emotion'], enforce_detection=False)
1584
+ dominant = result[0]["dominant_emotion"] if isinstance(result, list) else result["dominant_emotion"]
1585
+ emotion_counts[dominant] = emotion_counts.get(dominant, 0) + 1
1586
+ except Exception: pass
1587
+ frame_count += 1
1588
+ cap.release()
1589
+ if not emotion_counts: return "no_face"
1590
+ return max(emotion_counts.items(), key=lambda x: x[1])[0]
1591
+
1592
+ # Original Hugging Face model: HaniaRuby/speech-emotion-recognition-wav2vec2
1593
+ local_wav2vec_model_path = "HaniaRuby/speech-emotion-recognition-wav2vec2" # Local path to the downloaded model files
1594
+ print("🔁 Loading Wav2Vec processor and model...")
1595
+ wav2vec_processor = Wav2Vec2Processor.from_pretrained(local_wav2vec_model_path)
1596
+ wav2vec_model = Wav2Vec2ForSequenceClassification.from_pretrained(local_wav2vec_model_path)
1597
+ print("✅ Wav2Vec model loaded")
1598
+ wav2vec_model.eval()
1599
+ voice_label_map = {
1600
+ 0: 'angry', 1: 'disgust', 2: 'fear', 3: 'happy',
1601
+ 4: 'neutral', 5: 'sad', 6: 'surprise'
1602
+ }
1603
+
1604
+
1605
+
1606
+ def analyze_audio_emotion(audio_path):
1607
+ print(f"🔁 Analyzing audio emotion for: {audio_path}")
1608
+ if not audio_path or not os.path.exists(audio_path): return "neutral"
1609
+ speech, sr = librosa.load(audio_path, sr=16000)
1610
+ inputs = wav2vec_processor(speech, sampling_rate=16000, return_tensors="pt")
1611
+ with torch.no_grad():
1612
+ logits = wav2vec_model(**inputs).logits
1613
+ probs = torch.nn.functional.softmax(logits, dim=-1)
1614
+ predicted_id = torch.argmax(probs, dim=-1).item()
1615
+ return voice_label_map.get(predicted_id, "neutral")
1616
+
1617
+ # --- Effective confidence calculation
1618
+ def interpret_confidence(voice_label, face_label, answer_score_label, k=0.2):
1619
+ 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}
1620
+ answer_score_map = {"excellent": 1.0, "good": 0.8, "medium": 0.6, "poor": 0.3}
1621
+ 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)
1622
+ avg_emotion = (voice_score + face_score) / 2
1623
+ control_bonus = max(0, answer_score - avg_emotion) * k
1624
+ eff_conf = (0.5 * answer_score + 0.22 * voice_score + 0.18 * face_score + 0.1 * control_bonus)
1625
+ 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)}
1626
+
1627
+ seniority_mapping = {
1628
+ "Entry-level": 1, "Junior": 2, "Mid-Level": 3, "Senior": 4, "Lead": 5
1629
+ }
1630
+
1631
+
1632
+ # --- 2. Gradio App ---
1633
+
1634
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
1635
+ user_data = gr.State({})
1636
+ interview_state = gr.State({})
1637
+ missing_fields_state = gr.State([])
1638
+
1639
+ # --- UI Layout ---
1640
+ with gr.Column(visible=True) as user_info_section:
1641
+ gr.Markdown("## Candidate Information")
1642
+ cv_file = gr.File(label="Upload CV")
1643
+ job_desc = gr.Textbox(label="Job Description")
1644
+ start_btn = gr.Button("Continue", interactive=False)
1645
+
1646
+ with gr.Column(visible=False) as missing_section:
1647
+ gr.Markdown("## Missing Information")
1648
+ name_in = gr.Textbox(label="Name", visible=False)
1649
+ role_in = gr.Textbox(label="Job Role", visible=False)
1650
+ seniority_in = gr.Dropdown(list(seniority_mapping.keys()), label="Seniority", visible=False)
1651
+ skills_in = gr.Textbox(label="Skills", visible=False)
1652
+ submit_btn = gr.Button("Submit", interactive=False)
1653
+
1654
+ with gr.Column(visible=False) as interview_pre_section:
1655
+ pre_interview_greeting_md = gr.Markdown()
1656
+ start_interview_final_btn = gr.Button("Start Interview")
1657
+
1658
+ with gr.Column(visible=False) as interview_section:
1659
+ gr.Markdown("## Interview in Progress")
1660
+ question_audio = gr.Audio(label="Listen", interactive=False, autoplay=True)
1661
+ question_text = gr.Markdown()
1662
+ user_audio_input = gr.Audio(sources=["microphone"], type="filepath", label="1. Record Audio Answer")
1663
+ user_video_input = gr.Video(sources=["webcam"], label="2. Record Video Answer")
1664
+ stt_transcript = gr.Textbox(label="Transcribed Answer (edit if needed)")
1665
+ confirm_btn = gr.Button("Confirm Answer")
1666
+ evaluation_display = gr.Markdown()
1667
+ emotion_display = gr.Markdown()
1668
+ interview_summary = gr.Markdown(visible=False)
1669
+
1670
+ # --- UI Logic ---
1671
+
1672
+ def validate_start_btn(cv_file, job_desc):
1673
+ return gr.update(interactive=(cv_file is not None and hasattr(cv_file, "name") and bool(job_desc and job_desc.strip())))
1674
+ cv_file.change(validate_start_btn, [cv_file, job_desc], start_btn)
1675
+ job_desc.change(validate_start_btn, [cv_file, job_desc], start_btn)
1676
+
1677
+ def process_and_route_initial(cv_file, job_desc):
1678
+ details = extract_candidate_details(cv_file.name)
1679
+ job_info = extract_job_details(job_desc)
1680
+ data = {
1681
+ "name": details.get("name", "unknown"), "job_role": job_info.get("job_title", "unknown"),
1682
+ "seniority": job_info.get("experience_level", "unknown"), "skills": job_info.get("skills", [])
1683
+ }
1684
+ missing = [k for k, v in data.items() if (isinstance(v, str) and v.lower() == "unknown") or not v]
1685
+ if missing:
1686
+ return data, missing, gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
1687
+ else:
1688
+ greeting = f"Hello {data['name']}, your profile is ready. Click 'Start Interview' when ready."
1689
+ return data, missing, gr.update(visible=False), gr.update(visible=False), gr.update(visible=True, value=greeting)
1690
+ start_btn.click(
1691
+ process_and_route_initial,
1692
+ [cv_file, job_desc],
1693
+ [user_data, missing_fields_state, user_info_section, missing_section, pre_interview_greeting_md]
1694
+ )
1695
+
1696
+ def show_missing(missing):
1697
+ if missing is None: missing = []
1698
+ 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)
1699
+ missing_fields_state.change(show_missing, missing_fields_state, [name_in, role_in, seniority_in, skills_in])
1700
+
1701
+ def validate_fields(name, role, seniority, skills, missing):
1702
+ if not missing: return gr.update(interactive=False)
1703
+ 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())),])
1704
+ return gr.update(interactive=all_filled)
1705
+ for inp in [name_in, role_in, seniority_in, skills_in]:
1706
+ inp.change(validate_fields, [name_in, role_in, seniority_in, skills_in, missing_fields_state], submit_btn)
1707
+
1708
+ def complete_manual(data, name, role, seniority, skills):
1709
+ if data["name"].lower() == "unknown": data["name"] = name
1710
+ if data["job_role"].lower() == "unknown": data["job_role"] = role
1711
+ if data["seniority"].lower() == "unknown": data["seniority"] = seniority
1712
+ if not data["skills"]: data["skills"] = [s.strip() for s in skills.split(",")]
1713
+ greeting = f"Hello {data['name']}, your profile is ready. Click 'Start Interview' to begin."
1714
+ return data, gr.update(visible=False), gr.update(visible=True), gr.update(value=greeting)
1715
+ 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])
1716
+
1717
+ def start_interview(data):
1718
+ # --- Advanced state with full logging ---
1719
+ state = {
1720
+ "questions": [], "answers": [], "face_labels": [], "voice_labels": [], "timings": [],
1721
+ "question_evaluations": [], "answer_evaluations": [], "effective_confidences": [],
1722
+ "conversation_history": [],
1723
+ "difficulty_adjustment": None,
1724
+ "question_idx": 0, "max_questions": 3, "q_start_time": time.time(),
1725
+ "log": []
1726
+ }
1727
+ # --- Optionally: context retrieval here (currently just blank) ---
1728
+ context = ""
1729
+ prompt = build_interview_prompt(
1730
+ conversation_history=[], user_response="", context=context, job_role=data["job_role"],
1731
+ skills=data["skills"], seniority=data["seniority"], difficulty_adjustment=None,
1732
+ voice_label="neutral", face_label="neutral"
1733
+ )
1734
+ first_q = groq_llm.predict(prompt)
1735
+ # Evaluate Q for quality
1736
+ q_eval = eval_question_quality(first_q, data["job_role"], data["seniority"], None)
1737
+ state["questions"].append(first_q)
1738
+ state["question_evaluations"].append(q_eval)
1739
+ state["conversation_history"].append({'role': 'Interviewer', 'content': first_q})
1740
+ audio_path = bark_tts(first_q)
1741
+ # LOG
1742
+ state["log"].append({"type": "question", "question": first_q, "question_eval": q_eval, "timestamp": time.time()})
1743
+ return state, gr.update(visible=False), gr.update(visible=True), audio_path, f"*Question 1:* {first_q}"
1744
+ start_interview_final_btn.click(start_interview, [user_data], [interview_state, interview_pre_section, interview_section, question_audio, question_text])
1745
+
1746
+ def transcribe(audio_path):
1747
+ return whisper_stt(audio_path)
1748
+ user_audio_input.change(transcribe, user_audio_input, stt_transcript)
1749
+
1750
+ def process_answer(transcript, audio_path, video_path, state, data):
1751
+ if not transcript and not video_path:
1752
+ return state, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
1753
+ elapsed = round(time.time() - state.get("q_start_time", time.time()), 2)
1754
+ state["timings"].append(elapsed)
1755
+ state["answers"].append(transcript)
1756
+ state["conversation_history"].append({'role': 'Candidate', 'content': transcript})
1757
+
1758
+ # --- 1. Emotion analysis ---
1759
+ voice_label = analyze_audio_emotion(audio_path)
1760
+ face_label = analyze_video_emotions(video_path)
1761
+ state["voice_labels"].append(voice_label)
1762
+ state["face_labels"].append(face_label)
1763
+
1764
+ # --- 2. Evaluate previous Q and Answer ---
1765
+ last_q = state["questions"][-1]
1766
+ q_eval = state["question_evaluations"][-1] # Already in state
1767
+ ref_answer = generate_reference_answer(last_q, data["job_role"], data["seniority"])
1768
+ answer_eval = evaluate_answer(last_q, transcript, ref_answer, data["job_role"], data["seniority"], None)
1769
+ state["answer_evaluations"].append(answer_eval)
1770
+ answer_score = answer_eval.get("Score", "medium") if answer_eval else "medium"
1771
+
1772
+ # --- 3. Adaptive difficulty ---
1773
+ if answer_score == "excellent":
1774
+ state["difficulty_adjustment"] = "harder"
1775
+ elif answer_score in ("medium", "poor"):
1776
+ state["difficulty_adjustment"] = "easier"
1777
+ else:
1778
+ state["difficulty_adjustment"] = None
1779
+
1780
+ # --- 4. Effective confidence ---
1781
+ eff_conf = interpret_confidence(voice_label, face_label, answer_score)
1782
+ state["effective_confidences"].append(eff_conf)
1783
+
1784
+ # --- LOG ---
1785
+ state["log"].append({
1786
+ "type": "answer",
1787
+ "question": last_q,
1788
+ "answer": transcript,
1789
+ "answer_eval": answer_eval,
1790
+ "ref_answer": ref_answer,
1791
+ "face_label": face_label,
1792
+ "voice_label": voice_label,
1793
+ "effective_confidence": eff_conf,
1794
+ "timing": elapsed,
1795
+ "timestamp": time.time()
1796
+ })
1797
+
1798
+ # --- Next or End ---
1799
+ qidx = state["question_idx"] + 1
1800
+ if qidx >= state["max_questions"]:
1801
+ # Save as JSON (optionally)
1802
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
1803
+ log_file = f"interview_log_{timestamp}.json"
1804
+ with open(log_file, "w", encoding="utf-8") as f:
1805
+ json.dump(state["log"], f, indent=2, ensure_ascii=False)
1806
+ # Report
1807
+ summary = "# Interview Summary\n"
1808
+ for i, q in enumerate(state["questions"]):
1809
+ summary += (f"\n### Q{i + 1}: {q}\n"
1810
+ f"- *Answer*: {state['answers'][i]}\n"
1811
+ f"- *Q Eval*: {state['question_evaluations'][i]}\n"
1812
+ f"- *A Eval*: {state['answer_evaluations'][i]}\n"
1813
+ f"- *Face Emotion: {state['face_labels'][i]}, **Voice Emotion*: {state['voice_labels'][i]}\n"
1814
+ f"- *Effective Confidence*: {state['effective_confidences'][i]['effective_confidence']}\n"
1815
+ f"- *Time*: {state['timings'][i]}s\n")
1816
+ summary += f"\n\n⏺ Full log saved as {log_file}."
1817
+ 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}"))
1818
+ else:
1819
+ # --- Build next prompt using adaptive difficulty ---
1820
+ state["question_idx"] = qidx
1821
+ state["q_start_time"] = time.time()
1822
+ context = "" # You can add your context logic here
1823
+ prompt = build_interview_prompt(
1824
+ conversation_history=state["conversation_history"],
1825
+ user_response=transcript,
1826
+ context=context,
1827
+ job_role=data["job_role"],
1828
+ skills=data["skills"],
1829
+ seniority=data["seniority"],
1830
+ difficulty_adjustment=state["difficulty_adjustment"],
1831
+ face_label=face_label,
1832
+ voice_label=voice_label,
1833
+ effective_confidence=eff_conf
1834
+ )
1835
+ next_q = groq_llm.predict(prompt)
1836
+ # Evaluate Q quality
1837
+ q_eval = eval_question_quality(next_q, data["job_role"], data["seniority"], None)
1838
+ state["questions"].append(next_q)
1839
+ state["question_evaluations"].append(q_eval)
1840
+ state["conversation_history"].append({'role': 'Interviewer', 'content': next_q})
1841
+ state["log"].append({"type": "question", "question": next_q, "question_eval": q_eval, "timestamp": time.time()})
1842
+ audio_path = bark_tts(next_q)
1843
+ # Display evaluations
1844
+ eval_md = f"*Last Answer Eval:* {answer_eval}\n\n*Effective Confidence:* {eff_conf}"
1845
+ return (
1846
+ state, gr.update(visible=False), audio_path, f"*Question {qidx + 1}:* {next_q}",
1847
+ gr.update(value=None), gr.update(value=None),
1848
+ gr.update(visible=True, value=f"Last Detected — Face: {face_label}, Voice: {voice_label}"),
1849
+ )
1850
+ confirm_btn.click(
1851
+ process_answer,
1852
+ [stt_transcript, user_audio_input, user_video_input, interview_state, user_data],
1853
+ [interview_state, interview_summary, question_audio, question_text, user_audio_input, user_video_input, emotion_display]
1854
+ ).then(
1855
+ lambda: (gr.update(value=None), gr.update(value=None)), None, [user_audio_input, user_video_input]
1856
+ )
1857
+
1858
+ demo.launch(debug=True)
requirements.txt ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core Scientific Stack
2
+ numpy==1.24
3
+ scipy
4
+ soundfile
5
+ sounddevice
6
+ opencv-python==4.7.0.72
7
+ moviepy
8
+ librosa
9
+
10
+ # Hugging Face Transformers + Whisper + Bark
11
+ transformers
12
+ torch
13
+ sentence-transformers
14
+ git+https://github.com/openai/whisper.git
15
+ git+https://github.com/suno-ai/bark.git
16
+
17
+ # TTS + gTTS
18
+ TTS
19
+ gTTS
20
+
21
+ # Langchain ecosystem
22
+ langchain
23
+ langchain_groq
24
+ langchain_community
25
+ langchain_huggingface
26
+ llama-index
27
+ cohere
28
+
29
+ # Vector database
30
+ qdrant-client
31
+
32
+ # Emotion & face recognition
33
+ deepface
34
+ tensorflow
35
+ tf-keras
36
+
37
+ # Document parsing (CV/Job Description)
38
+ textract
39
+ PyPDF2
40
+ python-docx
41
+
42
+ # Audio I/O and video support
43
+ ffmpeg-python
44
+ pyaudio
45
+
46
+ # Misc tools
47
+ fuzzywuzzy
48
+ inputimeout
49
+ evaluate
50
+ datasets
51
+
52
+ # Ensure compatibility with pip
53
+ pip==23.3.1