Spaces:
Running
Running
Upload 2 files
Browse files- app.py +216 -0
- requirements.txt +9 -0
app.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
from sentence_transformers import SentenceTransformer
|
| 6 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 7 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 8 |
+
import os
|
| 9 |
+
|
| 10 |
+
# --- Configurations ---
|
| 11 |
+
DATASET_URL = "https://huggingface.co/spaces/Davichick/InterviewForge/resolve/main/interview_fordge_dataset_FINAL.csv"
|
| 12 |
+
EMBEDDINGS_PATH = "embeddings_MiniLM.npy"
|
| 13 |
+
EMBEDDING_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
|
| 14 |
+
GEN_MODEL_NAME = "Qwen/Qwen2.5-1.5B-Instruct"
|
| 15 |
+
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 16 |
+
|
| 17 |
+
# --- Load Data & Models ---
|
| 18 |
+
print("Loading dataset...")
|
| 19 |
+
try:
|
| 20 |
+
df = pd.read_csv(DATASET_URL, encoding="latin-1")
|
| 21 |
+
except Exception as e:
|
| 22 |
+
print(f"Failed to load dataset from URL: {e}")
|
| 23 |
+
# Fallback to local if running locally and URL fails
|
| 24 |
+
df = pd.read_csv("interview_fordge_dataset_FINAL.csv", encoding="latin-1")
|
| 25 |
+
df = df.reset_index(drop=True)
|
| 26 |
+
|
| 27 |
+
print("Loading embeddings...")
|
| 28 |
+
if os.path.exists(EMBEDDINGS_PATH):
|
| 29 |
+
embeddings = np.load(EMBEDDINGS_PATH, allow_pickle=True)
|
| 30 |
+
else:
|
| 31 |
+
# Dummy fallback to prevent crashing if file missing during build
|
| 32 |
+
embeddings = np.zeros((len(df), 384))
|
| 33 |
+
print(f"Warning: {EMBEDDINGS_PATH} not found. Embeddings initialized to zeros.")
|
| 34 |
+
|
| 35 |
+
print("Loading embedding model...")
|
| 36 |
+
embed_model = SentenceTransformer(EMBEDDING_MODEL_NAME)
|
| 37 |
+
|
| 38 |
+
print(f"Loading generation model {GEN_MODEL_NAME} on {DEVICE}...")
|
| 39 |
+
tokenizer = AutoTokenizer.from_pretrained(GEN_MODEL_NAME)
|
| 40 |
+
gen_model = AutoModelForCausalLM.from_pretrained(
|
| 41 |
+
GEN_MODEL_NAME,
|
| 42 |
+
torch_dtype=torch.bfloat16
|
| 43 |
+
).to(DEVICE)
|
| 44 |
+
gen_model.eval()
|
| 45 |
+
|
| 46 |
+
# --- Dropdown Constants ---
|
| 47 |
+
POSITIONS = ["Software Engineer", "Data Scientist", "Data Analyst", "Product Manager", "DevOps Engineer", "Machine Learning Engineer", "HR Specialist", "UX Designer", "Cybersecurity Analyst", "Marketing Specialist", "Finance Analyst", "Business Analyst", "Full Stack Developer", "Cloud Architect", "QA / Test Engineer", "Sales Representative", "Customer Success Specialist", "AI/ML Ops Engineer"]
|
| 48 |
+
SENIORITY_LEVELS = ["Junior", "Mid-Level", "Senior", "Lead", "Director"]
|
| 49 |
+
SECTORS = ["Technology", "Finance", "Healthcare", "E-Commerce", "Consulting", "Education", "Media & Entertainment", "Government", "Cybersecurity", "Real Estate & PropTech", "Legal & Compliance", "Gaming & Interactive Media", "Telecommunications", "Startups"]
|
| 50 |
+
INTERVIEW_TYPES = ["HR / Culture Fit", "Technical", "Behavioral (STAR method)", "System Design", "Case Study / Problem Solving"]
|
| 51 |
+
DIFFICULTY_LEVELS = ["Easy", "Medium", "Hard"]
|
| 52 |
+
|
| 53 |
+
# --- Helper Functions ---
|
| 54 |
+
def _generate(prompt, max_new_tokens=400, temperature=0.7):
|
| 55 |
+
inputs = tokenizer(prompt, return_tensors="pt").to(DEVICE)
|
| 56 |
+
with torch.no_grad():
|
| 57 |
+
output = gen_model.generate(
|
| 58 |
+
**inputs,
|
| 59 |
+
max_new_tokens=max_new_tokens,
|
| 60 |
+
temperature=temperature,
|
| 61 |
+
do_sample=True,
|
| 62 |
+
pad_token_id=tokenizer.eos_token_id
|
| 63 |
+
)
|
| 64 |
+
# Only decode the new tokens
|
| 65 |
+
new_tokens = output[0][inputs["input_ids"].shape[1]:]
|
| 66 |
+
return tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
|
| 67 |
+
|
| 68 |
+
def generate_model_answer(question, position, seniority, sector, interview_type, difficulty):
|
| 69 |
+
system_message = "You are an AI interview coach."
|
| 70 |
+
user_message = f"""You are an expert {seniority} {position} in the {sector} industry.
|
| 71 |
+
You are in a {difficulty} {interview_type} interview.
|
| 72 |
+
|
| 73 |
+
Answer the following interview question in 2-3 detailed paragraphs.
|
| 74 |
+
Be specific: mention real tools, frameworks, metrics, and methodologies.
|
| 75 |
+
Sound natural and confident, like a top-performing candidate.
|
| 76 |
+
|
| 77 |
+
Question: {question}"""
|
| 78 |
+
|
| 79 |
+
prompt = f"<|im_start|>system\n{system_message}<|im_end|>\n<|im_start|>user\n{user_message}<|im_end|>\n<|im_start|>assistant\n"
|
| 80 |
+
return _generate(prompt, max_new_tokens=400, temperature=0.7)
|
| 81 |
+
|
| 82 |
+
def evaluate_user_answer(question, reference_answer, user_answer):
|
| 83 |
+
system_message = "You are a strict but fair interview coach. Always respond in the exact format requested."
|
| 84 |
+
user_message = f"""Compare the candidate's answer against the reference answer and evaluate it.
|
| 85 |
+
|
| 86 |
+
Interview Question: {question[:300]}
|
| 87 |
+
|
| 88 |
+
Reference Answer (gold standard): {reference_answer[:500]}
|
| 89 |
+
|
| 90 |
+
Candidate's Answer: {user_answer[:500]}
|
| 91 |
+
|
| 92 |
+
Respond in EXACTLY this format:
|
| 93 |
+
Score: X/10
|
| 94 |
+
Strength: (one specific thing done well)
|
| 95 |
+
Weakness: (one specific thing missing or weak)
|
| 96 |
+
Tip: (one concrete actionable improvement)"""
|
| 97 |
+
|
| 98 |
+
prompt = f"<|im_start|>system\n{system_message}<|im_end|>\n<|im_start|>user\n{user_message}<|im_end|>\n<|im_start|>assistant\n"
|
| 99 |
+
raw_output = _generate(prompt, max_new_tokens=200, temperature=0.3)
|
| 100 |
+
return raw_output
|
| 101 |
+
|
| 102 |
+
# --- Gradio Callbacks ---
|
| 103 |
+
def set_quick_start(qs_type):
|
| 104 |
+
if qs_type == 1:
|
| 105 |
+
return "Data Scientist", "Senior", "Finance", "Technical", "Hard"
|
| 106 |
+
elif qs_type == 2:
|
| 107 |
+
return "Software Engineer", "Junior", "Technology", "Technical", "Medium"
|
| 108 |
+
elif qs_type == 3:
|
| 109 |
+
return "Product Manager", "Lead", "Startups", "Case Study / Problem Solving", "Hard"
|
| 110 |
+
|
| 111 |
+
def find_questions(position, seniority, sector, interview_type, difficulty, num_questions):
|
| 112 |
+
query = f"A {difficulty} {interview_type} interview question for a {seniority} {position} in the {sector} industry."
|
| 113 |
+
query_vector = embed_model.encode(query, convert_to_tensor=False)
|
| 114 |
+
|
| 115 |
+
mask = (
|
| 116 |
+
(df["position"] == position) &
|
| 117 |
+
(df["seniority"] == seniority) &
|
| 118 |
+
(df["sector"] == sector) &
|
| 119 |
+
(df["interview_type"] == interview_type) &
|
| 120 |
+
(df["difficulty"] == difficulty)
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
if mask.sum() == 0:
|
| 124 |
+
mask = pd.Series([True] * len(df))
|
| 125 |
+
|
| 126 |
+
original_indices = df[mask].index.tolist()
|
| 127 |
+
df_filtered = df[mask].reset_index(drop=True)
|
| 128 |
+
emb_filtered = embeddings[original_indices]
|
| 129 |
+
|
| 130 |
+
actual_k = min(int(num_questions), len(df_filtered))
|
| 131 |
+
scores = cosine_similarity([query_vector], emb_filtered)[0]
|
| 132 |
+
top_idx = scores.argsort()[::-1][:actual_k]
|
| 133 |
+
|
| 134 |
+
questions = []
|
| 135 |
+
for idx in top_idx:
|
| 136 |
+
q = df_filtered.iloc[idx]['question']
|
| 137 |
+
questions.append(q)
|
| 138 |
+
|
| 139 |
+
if not questions:
|
| 140 |
+
return gr.update(choices=[], value=None)
|
| 141 |
+
return gr.update(choices=questions, value=questions[0])
|
| 142 |
+
|
| 143 |
+
def submit_answer(selected_question, user_answer, enable_eval, position, seniority, sector, interview_type, difficulty):
|
| 144 |
+
if not selected_question:
|
| 145 |
+
return user_answer, "Please select a question first.", gr.update(value="", visible=False)
|
| 146 |
+
|
| 147 |
+
# Find the reference answer
|
| 148 |
+
match = df[df['question'] == selected_question]
|
| 149 |
+
reference_answer = match.iloc[0]['answer'] if len(match) > 0 else ""
|
| 150 |
+
|
| 151 |
+
model_ans = generate_model_answer(selected_question, position, seniority, sector, interview_type, difficulty)
|
| 152 |
+
|
| 153 |
+
if enable_eval:
|
| 154 |
+
eval_feedback = evaluate_user_answer(selected_question, reference_answer, user_answer)
|
| 155 |
+
return user_answer, model_ans, gr.update(value=eval_feedback, visible=True)
|
| 156 |
+
else:
|
| 157 |
+
return user_answer, model_ans, gr.update(value="", visible=False)
|
| 158 |
+
|
| 159 |
+
# --- Gradio UI Layout ---
|
| 160 |
+
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
| 161 |
+
gr.Markdown("# π― Interview Forge β AI Interview Coach")
|
| 162 |
+
gr.Markdown("Select your profile to find relevant interview questions, answer them, and get AI feedback.")
|
| 163 |
+
|
| 164 |
+
with gr.Row():
|
| 165 |
+
btn_qs1 = gr.Button("π Quick Start 1\nSr. Data Scientist")
|
| 166 |
+
btn_qs2 = gr.Button("π Quick Start 2\nJr. SWE Tech")
|
| 167 |
+
btn_qs3 = gr.Button("π Quick Start 3\nPM Lead Case")
|
| 168 |
+
|
| 169 |
+
with gr.Row():
|
| 170 |
+
pos_drop = gr.Dropdown(choices=POSITIONS, label="Position", value="Data Scientist")
|
| 171 |
+
sen_drop = gr.Dropdown(choices=SENIORITY_LEVELS, label="Seniority", value="Senior")
|
| 172 |
+
sec_drop = gr.Dropdown(choices=SECTORS, label="Sector", value="Finance")
|
| 173 |
+
|
| 174 |
+
with gr.Row():
|
| 175 |
+
type_drop = gr.Dropdown(choices=INTERVIEW_TYPES, label="Interview Type", value="Technical")
|
| 176 |
+
diff_drop = gr.Dropdown(choices=DIFFICULTY_LEVELS, label="Difficulty", value="Hard")
|
| 177 |
+
num_q_radio = gr.Radio(choices=["1", "2", "3", "4", "5"], label="How many questions?", value="3")
|
| 178 |
+
|
| 179 |
+
find_btn = gr.Button("π Find Questions", variant="primary")
|
| 180 |
+
|
| 181 |
+
gr.Markdown("### π Recommended Questions")
|
| 182 |
+
q_radio = gr.Radio(choices=[], label="Select a question to answer")
|
| 183 |
+
|
| 184 |
+
user_ans_box = gr.Textbox(lines=6, label="βοΈ Your Answer", placeholder="Type your interview answer here...")
|
| 185 |
+
eval_checkbox = gr.Checkbox(label="Enable AI Evaluation (score + feedback)", value=True)
|
| 186 |
+
|
| 187 |
+
submit_btn = gr.Button("π€ Submit Answer", variant="primary")
|
| 188 |
+
|
| 189 |
+
gr.Markdown("### π Results")
|
| 190 |
+
with gr.Row():
|
| 191 |
+
user_ans_display = gr.Textbox(label="Your Answer", interactive=False, lines=10)
|
| 192 |
+
model_ans_display = gr.Textbox(label="AI Model Answer", interactive=False, lines=10)
|
| 193 |
+
|
| 194 |
+
eval_display = gr.Textbox(label="AI Evaluation", interactive=False, visible=False, lines=5)
|
| 195 |
+
|
| 196 |
+
# Event wiring
|
| 197 |
+
btn_qs1.click(lambda: set_quick_start(1), inputs=None, outputs=[pos_drop, sen_drop, sec_drop, type_drop, diff_drop]).then(
|
| 198 |
+
find_questions, inputs=[pos_drop, sen_drop, sec_drop, type_drop, diff_drop, num_q_radio], outputs=[q_radio]
|
| 199 |
+
)
|
| 200 |
+
btn_qs2.click(lambda: set_quick_start(2), inputs=None, outputs=[pos_drop, sen_drop, sec_drop, type_drop, diff_drop]).then(
|
| 201 |
+
find_questions, inputs=[pos_drop, sen_drop, sec_drop, type_drop, diff_drop, num_q_radio], outputs=[q_radio]
|
| 202 |
+
)
|
| 203 |
+
btn_qs3.click(lambda: set_quick_start(3), inputs=None, outputs=[pos_drop, sen_drop, sec_drop, type_drop, diff_drop]).then(
|
| 204 |
+
find_questions, inputs=[pos_drop, sen_drop, sec_drop, type_drop, diff_drop, num_q_radio], outputs=[q_radio]
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
find_btn.click(find_questions, inputs=[pos_drop, sen_drop, sec_drop, type_drop, diff_drop, num_q_radio], outputs=[q_radio])
|
| 208 |
+
|
| 209 |
+
submit_btn.click(
|
| 210 |
+
submit_answer,
|
| 211 |
+
inputs=[q_radio, user_ans_box, eval_checkbox, pos_drop, sen_drop, sec_drop, type_drop, diff_drop],
|
| 212 |
+
outputs=[user_ans_display, model_ans_display, eval_display]
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
if __name__ == "__main__":
|
| 216 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch
|
| 2 |
+
transformers
|
| 3 |
+
gradio
|
| 4 |
+
sentence-transformers
|
| 5 |
+
scikit-learn
|
| 6 |
+
pandas
|
| 7 |
+
numpy
|
| 8 |
+
datasets
|
| 9 |
+
accelerate
|