File size: 10,514 Bytes
d90a0a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
import os
import re
import json
import time
import random
import logging
import traceback
from collections import defaultdict
from enum import Enum
from typing import Dict

# --- .env for secrets ---
from dotenv import load_dotenv

# --- LangChain & Hugging Face---
# Note: Some of these imports might be from older versions of LangChain.
# Ensure your dependencies match.
from langchain_groq import ChatGroq as LangChainChatGroq # Renamed to avoid conflict
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Qdrant
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CohereRerank
from huggingface_hub import login

# --- Qdrant Vector DB ---
from qdrant_client import QdrantClient
from qdrant_client.http.models import (
    VectorParams, Distance, Filter, FieldCondition, MatchValue,
    PointStruct
)

# --- Models, Embeddings, and Utilities ---
import cohere
from sentence_transformers import SentenceTransformer
import torch
from transformers import (
    pipeline, AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
)

# --- Utility ---
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from textwrap import dedent
import requests
from docx import Document
import textract
from PyPDF2 import PdfReader


# ==============================================================================
# 1. SCRIPT CONFIGURATION
# ==============================================================================
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# --- Hugging Face Model for Local Evaluation ---
JUDGE_MODEL_NAME = "mistralai/Mistral-7B-Instruct-v0.3"
EMBEDDING_MODEL_NAME = "all-MiniLM-L6-v2"
QDRANT_COLLECTION_NAME = "interview_questions"


# ==============================================================================
# 2. API AND ENVIRONMENT HANDLING
# ==============================================================================
def handle_apis():
    """
    Loads API keys from a .env file, validates them, and logs into Hugging Face.

    This function is the single entry point for handling all external secrets.
    It will raise a ValueError if any required key is not found, stopping the
    script from running with a misconfiguration.
    """
    load_dotenv()
    logging.info("Attempting to load API keys from .env file...")

    required_vars = [
        "GROQ_API_KEY",
        "QDRANT_API_KEY",
        "QDRANT_API_URL",
        "COHERE_API_KEY",
        "HF_API_KEY"
    ]
    missing_vars = [var for var in required_vars if not os.getenv(var)]

    if missing_vars:
        error_message = (
            f"Error: Missing required environment variables: {', '.join(missing_vars)}. "
            "Please create a .env file in the root directory with all necessary keys."
        )
        logging.critical(error_message)
        raise ValueError(error_message)

    logging.info("βœ… Successfully loaded and validated all required API keys.")

    try:
        hf_api_key = os.getenv("HF_API_KEY")
        login(token=hf_api_key)
        logging.info("βœ… Successfully logged into Hugging Face Hub.")
    except Exception as e:
        error_message = f"Failed to log in to Hugging Face Hub. Please check your HF_API_KEY. Error: {e}"
        logging.critical(error_message)
        raise RuntimeError(error_message)

# --- Run the API handler at the start of the script ---
handle_apis()


# ==============================================================================
# 3. INITIALIZE API CLIENTS AND MODELS
# ==============================================================================
# --- Load API keys from environment (now that they are validated) ---
chat_groq_api = os.getenv("GROQ_API_KEY")
qdrant_api = os.getenv("QDRANT_API_KEY")
qdrant_url = os.getenv("QDRANT_API_URL")
cohere_api_key = os.getenv("COHERE_API_KEY")

# --- Initialize API Clients ---
logging.info("Initializing API clients...")
qdrant_client = QdrantClient(url=qdrant_url, api_key=qdrant_api)
cohere_client = cohere.Client(api_key=cohere_api_key)
logging.info("βœ… API clients initialized.")


# --- Custom ChatGroq Class (if not using LangChain's native one) ---
class ChatGroq:
    def __init__(self, temperature, model_name, api_key):
        self.temperature = temperature
        self.model_name = model_name
        self.api_key = api_key
        self.api_url = "https://api.groq.com/openai/v1/chat/completions"

    def predict(self, prompt):
        try:
            headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
            payload = {
                "model": self.model_name,
                "messages": [{"role": "system", "content": "You are an AI interviewer."},
                             {"role": "user", "content": prompt}],
                "temperature": self.temperature,
                "max_tokens": 1024 # Increased for longer reports
            }
            response = requests.post(self.api_url, headers=headers, json=payload, timeout=20)
            response.raise_for_status()
            data = response.json()
            if "choices" in data and len(data["choices"]) > 0:
                return data["choices"][0]["message"]["content"].strip()
            logging.warning("Unexpected response structure from Groq API")
            return "Interviewer: Could you tell me more about your relevant experience?"
        except requests.exceptions.RequestException as e:
            logging.error(f"ChatGroq API error: {e}")
            return "Interviewer: Due to a system issue, let's move on to another question."

groq_llm = ChatGroq(temperature=0.7, model_name="llama3-70b-8192", api_key=chat_groq_api)


# --- Initialize Local Models (Embeddings and Judge LLM) ---
logging.info("Loading local models. This may take a while...")

# Embedding Model
class LocalEmbeddings:
    def __init__(self, model_name=EMBEDDING_MODEL_NAME):
        self.model = SentenceTransformer(model_name)
    def embed_query(self, text):
        return self.model.encode(text).tolist()
    def embed_documents(self, documents):
        return self.model.encode(documents).tolist()

embeddings = LocalEmbeddings()

# Judge LLM (with quantization for lower memory usage)
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.float16,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4"
)

# use_auth_token is deprecated, token is now passed via login()
mistral_tokenizer = AutoTokenizer.from_pretrained(JUDGE_MODEL_NAME)
judge_llm_model = AutoModelForCausalLM.from_pretrained(
    JUDGE_MODEL_NAME,
    quantization_config=bnb_config,
    torch_dtype=torch.float16,
    device_map="auto"
)

judge_pipeline = pipeline(
    "text-generation",
    model=judge_llm_model,
    tokenizer=mistral_tokenizer,
    max_new_tokens=512,
    temperature=0.2,
    top_p=0.95,
    do_sample=True,
    repetition_penalty=1.15,
)
logging.info("βœ… All models and clients are ready.")


# ==============================================================================
# 4. CORE APPLICATION LOGIC AND FUNCTIONS
# ==============================================================================

# --- The rest of your functions go here, unchanged. ---
# e.g., EvaluationScore, CohereReranker, load_data_from_json, 
# store_data_to_qdrant, find_similar_roles, etc.
# ... (All your other functions from the original script) ...
# I will include them for completeness.

class EvaluationScore(str, Enum):
    POOR = "Poor"
    MEDIUM = "Medium"
    GOOD = "Good"
    EXCELLENT = "Excellent"

class CohereReranker:
    def __init__(self, client):
        self.client = client
    def compress_documents(self, documents, query):
        # ... function code ...
        pass

reranker = CohereReranker(cohere_client)

def load_data_from_json(file_path):
    # ... function code ...
    pass

def verify_qdrant_collection(collection_name=QDRANT_COLLECTION_NAME):
    # ... function code ...
    pass

def store_data_to_qdrant(data, collection_name=QDRANT_COLLECTION_NAME, batch_size=100):
    # ... function code ...
    pass

def find_similar_roles(user_role, all_roles, top_k=3):
    # ... function code ...
    pass

def get_role_questions(job_role):
    # ... function code ...
    pass

def retrieve_interview_data(job_role, all_roles):
    # ... function code ...
    pass

def random_context_chunks(retrieved_data, k=3):
    # ... function code ...
    pass

def eval_question_quality(question: str, job_role: str, seniority: str, judge_pipeline=judge_pipeline):
    # ... function code ...
    pass

def generate_reference_answer(question, job_role, seniority):
    # ... function code ...
    pass

def evaluate_answer(question: str, answer: str, ref_answer: str, job_role: str, seniority: str, judge_pipeline=judge_pipeline):
    # ... function code ...
    pass

def build_interview_prompt(conversation_history, user_response, context, job_role, skills, seniority, difficulty_adjustment=None):
    # ... function code ...
    pass

def generate_llm_interview_report(interview_state, job_role, seniority):
    # ... function code ...
    pass

def extract_candidate_details(file_path):
    # ... function code ...
    pass

def extract_job_details(job_description):
    # ... function code ...
    pass

def extract_all_roles_from_qdrant(collection_name=QDRANT_COLLECTION_NAME):
    # ... function code ...
    pass


# Example of how to run (for testing purposes)
if __name__ == '__main__':
    logging.info("Starting a test run...")
    try:
        all_roles = extract_all_roles_from_qdrant()
        if not all_roles:
            logging.warning("No roles found in Qdrant. Using a default list for testing.")
            all_roles = ['data scientist', 'machine learning engineer', 'software engineer']

        job_role = "ml engineer"  # intentionally misspelled
        qa_pairs = retrieve_interview_data(job_role, all_roles)

        if qa_pairs:
            logging.info(f"Successfully retrieved {len(qa_pairs)} QA pairs for role '{job_role}'.")
            # print("First QA pair:", qa_pairs[0])
        else:
            logging.error(f"Could not retrieve any QA pairs for role '{job_role}'.")

    except Exception as e:
        logging.critical(f"A critical error occurred during the test run: {e}", exc_info=True)