Davichick commited on
Commit
8a66298
·
verified ·
1 Parent(s): 347d4d9

Upload 2 files

Browse files
Files changed (3) hide show
  1. .gitattributes +1 -0
  2. app.py +270 -0
  3. logo.png +3 -0
.gitattributes CHANGED
@@ -35,3 +35,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  interview_forge_dataset_FINAL.csv filter=lfs diff=lfs merge=lfs -text
37
  interview_forge_v3_complete.csv filter=lfs diff=lfs merge=lfs -text
 
 
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  interview_forge_dataset_FINAL.csv filter=lfs diff=lfs merge=lfs -text
37
  interview_forge_v3_complete.csv filter=lfs diff=lfs merge=lfs -text
38
+ logo.png filter=lfs diff=lfs merge=lfs -text
app.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import numpy as np
4
+ import os
5
+ import re
6
+ import torch
7
+ from transformers import pipeline
8
+ from sentence_transformers import SentenceTransformer
9
+ from sklearn.metrics.pairwise import cosine_similarity
10
+
11
+ print("Loading E5 Retrieval Model and Embeddings...")
12
+
13
+ base_dir = os.path.dirname(__file__)
14
+ csv_path = os.path.join(base_dir, 'interview_forge_v3_complete.csv')
15
+ if not os.path.exists(csv_path):
16
+ csv_path = os.path.join(base_dir, '..', 'interview_forge_v3_complete.csv')
17
+
18
+ npy_path = os.path.join(base_dir, 'e5_npu_full_embeddings.npy')
19
+ if not os.path.exists(npy_path):
20
+ npy_path = os.path.join(base_dir, 'e5_full_embeddings.npy')
21
+ if not os.path.exists(npy_path):
22
+ npy_path = os.path.join(base_dir, '..', 'e5_npu_full_embeddings.npy')
23
+ if not os.path.exists(npy_path):
24
+ npy_path = os.path.join(base_dir, '..', 'e5_full_embeddings.npy')
25
+
26
+ df = pd.read_csv(csv_path).dropna(subset=['question']).reset_index(drop=True)
27
+ full_embeddings = np.load(npy_path)
28
+
29
+ final_model = SentenceTransformer("intfloat/e5-small-v2")
30
+
31
+ model_id = "Qwen/Qwen2.5-1.5B-Instruct"
32
+ print(f"Loading {model_id} into memory...")
33
+ generator = pipeline("text-generation", model=model_id, torch_dtype=torch.bfloat16, device="cpu")
34
+ print("Models loaded successfully!")
35
+
36
+ roles = sorted(df['role'].unique().tolist())
37
+ sectors = sorted(df['sector'].unique().tolist())
38
+ interviewers = ["Strict Technical Lead", "Friendly HR Manager", "Aggressive CISO", "Curious Senior Developer", "Business-Focused Product Manager"]
39
+ levels = ["Junior", "Mid-Level", "Senior", "Expert", "Manager"]
40
+
41
+
42
+ def get_interview_question(user_role, user_sector, user_interviewer, user_level):
43
+ query_text = f"An interview question for a {user_level} {user_role} in the {user_sector} sector, asked by a {user_interviewer}."
44
+ query_embedding = final_model.encode([f"query: {query_text}"], normalize_embeddings=True)
45
+ similarities = cosine_similarity(query_embedding, full_embeddings)[0]
46
+ best_match_idx = similarities.argsort()[::-1][0]
47
+ return df.iloc[best_match_idx]['question']
48
+
49
+
50
+ def get_more_like_this(user_role, user_sector, current_question):
51
+ if not current_question:
52
+ return "Please generate a question first."
53
+
54
+ # -------------------------------------------------------------
55
+ # NEW LOGIC: Filter strictly by the SAME Role and Sector
56
+ # -------------------------------------------------------------
57
+ filtered_df = df[(df['role'] == user_role) & (df['sector'] == user_sector)]
58
+
59
+ if filtered_df.empty:
60
+ filtered_df = df # Fallback if combination is completely missing
61
+
62
+ # Remove the exact question we are looking at so we don't repeat it
63
+ pool = filtered_df[filtered_df['question'] != current_question]
64
+
65
+ if pool.empty:
66
+ pool = filtered_df
67
+
68
+ # Pick a random question from this highly relevant pool!
69
+ random_match = pool.sample(n=1).iloc[0]['question']
70
+ return random_match
71
+
72
+
73
+ def create_circular_progress(grade_text):
74
+ match = re.search(r'Grade:\s*(\d+)', grade_text)
75
+ if match:
76
+ score = int(match.group(1))
77
+ else:
78
+ score = 0
79
+
80
+ percentage = (score / 10) * 100
81
+ dasharray = f"{percentage} {100 - percentage}"
82
+
83
+ if score >= 8:
84
+ color = "#4ade80"
85
+ elif score >= 5:
86
+ color = "#facc15"
87
+ else:
88
+ color = "#f87171"
89
+
90
+ svg_html = f"""
91
+ <div style="display: flex; justify-content: center; align-items: center; padding: 20px; flex-direction: column;">
92
+ <h3 style="margin-bottom: 15px; color: #cbd5e1; font-family: sans-serif;">AI Grade</h3>
93
+ <div style="position: relative; width: 150px; height: 150px;">
94
+ <svg viewBox="0 0 36 36" style="width: 100%; height: 100%;">
95
+ <path
96
+ d="M18 2.0845
97
+ a 15.9155 15.9155 0 0 1 0 31.831
98
+ a 15.9155 15.9155 0 0 1 0 -31.831"
99
+ fill="none"
100
+ stroke="#1e293b"
101
+ stroke-width="3"
102
+ />
103
+ <path
104
+ d="M18 2.0845
105
+ a 15.9155 15.9155 0 0 1 0 31.831
106
+ a 15.9155 15.9155 0 0 1 0 -31.831"
107
+ fill="none"
108
+ stroke="{color}"
109
+ stroke-width="3"
110
+ stroke-dasharray="{dasharray}"
111
+ style="transition: stroke-dasharray 1s ease-out;"
112
+ />
113
+ </svg>
114
+ <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 28px; font-weight: bold; color: #f8fafc; font-family: sans-serif;">
115
+ {score}/10
116
+ </div>
117
+ </div>
118
+ </div>
119
+ """
120
+ return svg_html
121
+
122
+ def evaluate_and_format(question_text, candidate_answer, user_role, user_sector, user_interviewer, user_level):
123
+ if not candidate_answer.strip():
124
+ return "", "Please type an answer before submitting."
125
+
126
+ system_prompt = f"""You are a {user_interviewer} evaluating a {user_level} {user_role} candidate in the {user_sector} sector.
127
+
128
+ CRITICAL RULES:
129
+ 1. Speak DIRECTLY to the candidate using "you" and "your". Never use the word "candidate".
130
+ 2. Separate the prompt context. Do NOT penalize or critique the user for constraints that were mentioned in the [INTERVIEW QUESTION].
131
+ 3. You MUST generate an Example Answer at the very end. Keep it extremely short.
132
+
133
+ You MUST output exactly this format and nothing else:
134
+ Grade: [1-10]/10
135
+ Pros:
136
+ - [Pro 1]
137
+ - [Pro 2]
138
+ Cons:
139
+ - [Con 1]
140
+ - [Con 2]
141
+ Example Answer:
142
+ [Provide a strict maximum 2-sentence example of a perfect answer.]"""
143
+
144
+ messages = [
145
+ {"role": "system", "content": system_prompt},
146
+ {"role": "user", "content": f"[INTERVIEW QUESTION]\n{question_text}\n\n[CANDIDATE'S ANSWER]\n{candidate_answer}"}
147
+ ]
148
+
149
+ outputs = generator(messages, max_new_tokens=400, temperature=0.7, do_sample=True)
150
+ raw_feedback = outputs[0]['generated_text'][-1]['content']
151
+
152
+ score_html = create_circular_progress(raw_feedback)
153
+ clean_feedback = re.sub(r'Grade:.*?\n', '', raw_feedback).strip()
154
+
155
+ return score_html, clean_feedback
156
+
157
+ # =====================================================================
158
+ # UI DESIGN & THEME INJECTION
159
+ # =====================================================================
160
+
161
+ custom_css = """
162
+ /* Force the background to be a very dark navy/black to blend with the logo */
163
+ body, .gradio-container {
164
+ background-color: #040f23 !important;
165
+ }
166
+ /* Center and constrain the width of the dropdown block */
167
+ .centered-dropdowns {
168
+ max-width: 800px !important;
169
+ margin: 0 auto !important;
170
+ }
171
+ /* Smaller, centered main generate button */
172
+ .center-btn {
173
+ max-width: 250px !important;
174
+ margin: 20px auto !important;
175
+ display: block !important;
176
+ }
177
+ /* Ensure the question and answer columns are the same height */
178
+ .side-by-side {
179
+ align-items: stretch !important;
180
+ }
181
+ """
182
+
183
+ # Implement a dark, professional theme that utilizes Gold (#d4af37) as an accent color
184
+ theme = gr.themes.Default(
185
+ primary_hue="amber",
186
+ secondary_hue="blue",
187
+ neutral_hue="slate",
188
+ ).set(
189
+ body_background_fill="#040f23",
190
+ body_text_color="#f8fafc",
191
+ block_background_fill="#0b172a",
192
+ block_label_text_color="#cbd5e1",
193
+ button_primary_background_fill="#d4af37",
194
+ button_primary_text_color="#000000",
195
+ button_secondary_background_fill="#1e293b",
196
+ button_secondary_text_color="#f8fafc"
197
+ )
198
+
199
+ with gr.Blocks(theme=theme, css=custom_css) as app:
200
+
201
+ # Render the logo directly using HTML. It will look for "logo.png" in the HF Space directory.
202
+ gr.HTML(
203
+ """
204
+ <div style="text-align: center; margin-bottom: 20px;">
205
+ <img src="file/logo.png" style="max-height: 250px; margin: 0 auto; display: block;" alt="Interview Forge"/>
206
+ </div>
207
+ """
208
+ )
209
+
210
+ # -------------------------------------------------------------
211
+ # Centered Dropdown Block
212
+ # -------------------------------------------------------------
213
+ with gr.Column(elem_classes="centered-dropdowns"):
214
+ with gr.Row():
215
+ role_dropdown = gr.Dropdown(choices=roles, label="Role", value=roles[0] if roles else None)
216
+ sector_dropdown = gr.Dropdown(choices=sectors, label="Sector", value=sectors[0] if sectors else None)
217
+ with gr.Row():
218
+ interviewer_dropdown = gr.Dropdown(choices=interviewers, label="Interviewer Persona", value=interviewers[0])
219
+ level_dropdown = gr.Dropdown(choices=levels, label="Difficulty Level", value=levels[1])
220
+
221
+ generate_btn = gr.Button("Generate Custom Question", variant="primary", elem_classes="center-btn")
222
+
223
+ gr.Markdown("---")
224
+
225
+ # -------------------------------------------------------------
226
+ # Side-by-Side Question and Answer block
227
+ # -------------------------------------------------------------
228
+ with gr.Row(elem_classes="side-by-side"):
229
+ with gr.Column():
230
+ question_display = gr.Textbox(label="The Question", interactive=False, lines=10, text_align="left")
231
+ more_btn = gr.Button("More Like This", variant="secondary")
232
+ with gr.Column():
233
+ user_answer = gr.Textbox(label="Your Answer", lines=10, placeholder="Type your answer here...")
234
+ submit_btn = gr.Button("Submit Answer for AI Grading", variant="primary")
235
+
236
+ gr.Markdown("---")
237
+
238
+ # -------------------------------------------------------------
239
+ # Feedback Output
240
+ # -------------------------------------------------------------
241
+ with gr.Row():
242
+ with gr.Column(scale=1, min_width=200):
243
+ score_circle = gr.HTML()
244
+ with gr.Column(scale=3):
245
+ feedback_display = gr.Markdown(label="AI Feedback")
246
+
247
+ # -------------------------------------------------------------
248
+ # Event Wires
249
+ # -------------------------------------------------------------
250
+ generate_btn.click(
251
+ fn=get_interview_question,
252
+ inputs=[role_dropdown, sector_dropdown, interviewer_dropdown, level_dropdown],
253
+ outputs=question_display
254
+ )
255
+
256
+ # The new "More Like This" logic passes the Role and Sector as well to filter the dataframe!
257
+ more_btn.click(
258
+ fn=get_more_like_this,
259
+ inputs=[role_dropdown, sector_dropdown, question_display],
260
+ outputs=question_display
261
+ )
262
+
263
+ submit_btn.click(
264
+ fn=evaluate_and_format,
265
+ inputs=[question_display, user_answer, role_dropdown, sector_dropdown, interviewer_dropdown, level_dropdown],
266
+ outputs=[score_circle, feedback_display]
267
+ )
268
+
269
+ if __name__ == "__main__":
270
+ app.launch(server_name="0.0.0.0", server_port=7860, share=False)
logo.png ADDED

Git LFS Details

  • SHA256: 10439b5d28760014aadb6078ba8e8aa008441a5355850be55174a731905424e5
  • Pointer size: 132 Bytes
  • Size of remote file: 7.82 MB