curiouscurrent commited on
Commit
c814146
·
verified ·
1 Parent(s): 7de0953

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +101 -57
app.py CHANGED
@@ -2,20 +2,25 @@ import gradio as gr
2
  import pandas as pd
3
  import json
4
  import os
 
5
  import re
6
- from sklearn.feature_extraction.text import TfidfVectorizer
7
- from sklearn.metrics.pairwise import cosine_similarity
8
  from functools import lru_cache
9
 
10
  # ----------------------------
11
  # CONFIG
12
  # ----------------------------
13
  JSON_FILE = "form-submissions-1.json"
 
 
 
 
14
  FILTERED_CSV = "/tmp/filtered_candidates.csv"
 
 
15
 
16
- # Note: The following variables are no longer needed, as we are not using the Hugging Face API
17
- # MODEL_ID = "google/flan-t5-small"
18
- # HF_API_TOKEN = os.environ.get("HF_API_TOKEN")
19
 
20
  CATEGORIES = {
21
  "AI": [
@@ -32,38 +37,60 @@ CATEGORIES = {
32
  }
33
 
34
  # ----------------------------
35
- # Similarity Matching Function (REPLACING LLM)
36
  # ----------------------------
37
- @lru_cache(maxsize=1)
38
- def calculate_similarity_scores(df_candidates, category_name):
39
- """
40
- Calculates the cosine similarity between candidate roles and target job titles.
41
- """
42
- if df_candidates.empty:
43
- return pd.Series([], dtype='float64')
44
-
45
- # 1. Define the document corpus
46
- target_roles = " ".join(CATEGORIES[category_name])
47
- candidate_roles = df_candidates['Roles'].tolist()
 
 
 
 
 
 
 
 
48
 
49
- # 2. Create the corpus for vectorization
50
- corpus = [target_roles] + candidate_roles
 
 
 
 
 
 
51
 
52
- # 3. Vectorize using TF-IDF (converts text to numerical features)
53
- # Uses unigrams and bigrams for better matching (e.g., 'Data Scientist' vs 'Data' 'Scientist')
54
- vectorizer = TfidfVectorizer(ngram_range=(1, 2), stop_words='english')
55
- tfidf_matrix = vectorizer.fit_transform(corpus)
56
-
57
- # 4. Extract the vector for the target roles (the first row)
58
- target_vector = tfidf_matrix[0]
59
- candidate_vectors = tfidf_matrix[1:]
60
-
61
- # 5. Calculate Cosine Similarity between the target and all candidates
62
- # The result is a matrix where [0, i] is the similarity score for candidate i
63
- similarity_scores = cosine_similarity(target_vector, candidate_vectors).flatten()
64
-
65
- # Return scores as a Pandas Series aligned with the DataFrame index
66
- return pd.Series(similarity_scores, index=df_candidates.index)
 
 
 
 
 
 
 
 
67
 
68
  # ----------------------------
69
  # Step 1: Filter by roles (Unchanged)
@@ -106,13 +133,15 @@ def filter_by_roles(category_name):
106
 
107
  df = pd.DataFrame(filtered)
108
  df.to_csv(FILTERED_CSV, index=False)
109
- return df, f"{len(df)} candidates filtered by role for category '{category_name}'. Ready for Similarity Ranking."
110
 
111
 
112
  # ----------------------------
113
- # Step 2: Recommendations (Using Similarity Matching)
114
  # ----------------------------
115
- def similarity_recommendations(category_name):
 
 
116
  if not os.path.exists(FILTERED_CSV):
117
  df_filtered, msg = filter_by_roles(category_name)
118
  if df_filtered.empty:
@@ -124,16 +153,35 @@ def similarity_recommendations(category_name):
124
  if df_filtered.empty:
125
  return f"No filtered candidates found for category '{category_name}'. Run Step 1 first."
126
 
127
- # --- CORE CHANGE: Calculate Similarity Scores ---
128
- df_filtered["Similarity_Score"] = calculate_similarity_scores(df_filtered, category_name)
129
 
130
- # Filter out candidates with zero relevance (can happen if roles are too generic)
131
- df_recommended = df_filtered[df_filtered["Similarity_Score"] > 0].copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
  if df_recommended.empty:
134
- return f"All candidates had zero text similarity to the target roles for '{category_name}'. The current roles are not specific enough to match."
 
 
135
 
136
- # Define salary parsing for tie-breaker
137
  def parse_salary(s):
138
  try:
139
  return float(str(s).replace("$","").replace(",","").replace("N/A", str(float('inf'))))
@@ -142,9 +190,8 @@ def similarity_recommendations(category_name):
142
 
143
  df_recommended["Salary_sort"] = df_recommended["Salary"].apply(parse_salary)
144
 
145
- # Sort: 1. Highest Similarity Score (descending), 2. Lowest Salary (ascending)
146
  df_top5 = df_recommended.sort_values(
147
- by=['Similarity_Score', 'Salary_sort'],
148
  ascending=[False, True]
149
  ).head(5)
150
 
@@ -153,12 +200,10 @@ def similarity_recommendations(category_name):
153
  output_text = f"Top {len(final_names)} Recommended Candidates for the '{category_name}' Category:\n\n"
154
 
155
  for i, name in enumerate(final_names):
156
- score = df_top5.iloc[i]['Similarity_Score']
157
- # Display the score as a percentage for readability
158
- score_percent = f"{score * 100:.2f}%"
159
- output_text += f"{i+1}. {name} (Role Match: {score_percent})\n"
160
 
161
- output_text += "\nThese candidates were ranked objectively based on the **text similarity** of their previous job roles to the target roles, using expected salary as a tie-breaker."
162
 
163
  return output_text
164
 
@@ -179,8 +224,8 @@ def show_first_candidates():
179
  # Gradio interface (Updated Heading and Launch)
180
  # ----------------------------
181
  with gr.Blocks() as app:
182
- gr.Markdown("# 🏆 Candidate Selection (Objective Similarity Matching)")
183
- gr.Markdown("### **Reliable ranking using text similarity (TF-IDF & Cosine Similarity) - No LLM API required.**")
184
 
185
  gr.Markdown("#### 🔍 Raw JSON Preview: First 5 Candidates")
186
  gr.Dataframe(show_first_candidates(), label="First 5 JSON Entries")
@@ -196,11 +241,10 @@ with gr.Blocks() as app:
196
 
197
  gr.Markdown("---")
198
 
199
- # Step 2: Recommendations
200
- # Changed function name to reflect the new logic
201
- recommend_button = gr.Button("3. Rank Candidates by Role Similarity")
202
- recommend_output_text = gr.Textbox(label="Top Candidate Recommendations Summary", lines=10, placeholder="Click 'Rank Candidates by Role Similarity' after Step 2 completes.")
203
- recommend_button.click(similarity_recommendations, inputs=[category_dropdown], outputs=[recommend_output_text])
204
 
205
  if __name__ == "__main__":
206
  app.launch(share=True)
 
2
  import pandas as pd
3
  import json
4
  import os
5
+ import requests
6
  import re
 
 
7
  from functools import lru_cache
8
 
9
  # ----------------------------
10
  # CONFIG
11
  # ----------------------------
12
  JSON_FILE = "form-submissions-1.json"
13
+ # 🚩 CHANGE: Switched to a more capable, instruction-tuned model for semantic matching
14
+ MODEL_ID = "google/flan-t5-large"
15
+ # NOTE: HF_API_TOKEN MUST be set in your environment variables/Space secrets.
16
+ HF_API_TOKEN = os.environ.get("HF_API_TOKEN")
17
  FILTERED_CSV = "/tmp/filtered_candidates.csv"
18
+ OUTPUT_FILE = "/tmp/outputs.csv"
19
+ BATCH_SIZE = 50
20
 
21
+ if not HF_API_TOKEN:
22
+ # Allow launch for demonstration, but function will warn if token is missing
23
+ pass
24
 
25
  CATEGORIES = {
26
  "AI": [
 
37
  }
38
 
39
  # ----------------------------
40
+ # LLM Call for Semantic Role Scoring
41
  # ----------------------------
42
+ @lru_cache(maxsize=512)
43
+ def score_candidate(candidate_str, category_name, job_titles_tuple):
44
+ if not HF_API_TOKEN:
45
+ print("API Token is missing. Returning score 0.")
46
+ return 0
47
+
48
+ # 🚩 PROMPT CHANGE: Focus on 'semantic relevance' and 'conceptual fit'
49
+ prompt = f"""
50
+ You are an HR expert performing semantic matching. Your task is to rate a candidate's conceptual fit based ONLY on their previous job roles and the target roles.
51
+ Rate the semantic relevance of the candidate's 'Roles' to the 'Target Roles' on a scale of 1 (Lowest Match) to 10 (Highest Semantic Match).
52
+ The score must reflect the conceptual alignment and industry similarity, not just keyword presence.
53
+
54
+ The target roles for the '{category_name}' category are: {list(job_titles_tuple)}
55
+
56
+ Candidate JSON: {candidate_str}
57
+
58
+ **Task**: Respond ONLY with the rating number (an integer from 1 to 10).
59
+ """
60
+ headers = {"Authorization": f"Bearer {HF_API_TOKEN}", "Content-Type": "application/json"}
61
 
62
+ payload = {
63
+ "inputs": prompt,
64
+ "parameters": {
65
+ "max_new_tokens": 5,
66
+ "return_full_text": False,
67
+ "temperature": 0.1
68
+ }
69
+ }
70
 
71
+ try:
72
+ # Note: Flan-T5-Large is slower than small, but more powerful for this task
73
+ response = requests.post(
74
+ f"https://api-inference.huggingface.co/models/{MODEL_ID}",
75
+ headers=headers,
76
+ data=json.dumps(payload),
77
+ timeout=120 # Increased timeout for the larger model
78
+ )
79
+ response.raise_for_status()
80
+ result = response.json()
81
+
82
+ generated_text = result[0].get("generated_text", "0").strip()
83
+
84
+ match = re.search(r'\d+', generated_text)
85
+ if match:
86
+ score = int(match.group(0))
87
+ return max(1, min(10, score))
88
+
89
+ return 0
90
+
91
+ except Exception as e:
92
+ print(f"LLM scoring call failed for candidate (API/Network Error): {e}")
93
+ return 0
94
 
95
  # ----------------------------
96
  # Step 1: Filter by roles (Unchanged)
 
133
 
134
  df = pd.DataFrame(filtered)
135
  df.to_csv(FILTERED_CSV, index=False)
136
+ return df, f"{len(df)} candidates filtered by role for category '{category_name}'. Ready for LLM Semantic Scoring."
137
 
138
 
139
  # ----------------------------
140
+ # Step 2: LLM recommendations (Semantic Scoring, Sorting, and Output)
141
  # ----------------------------
142
+ def llm_recommendations(category_name):
143
+ job_titles = CATEGORIES[category_name]
144
+
145
  if not os.path.exists(FILTERED_CSV):
146
  df_filtered, msg = filter_by_roles(category_name)
147
  if df_filtered.empty:
 
153
  if df_filtered.empty:
154
  return f"No filtered candidates found for category '{category_name}'. Run Step 1 first."
155
 
156
+ df_filtered_clean = df_filtered.fillna('N/A')
157
+ filtered_candidates = df_filtered_clean.to_dict(orient="records")
158
 
159
+ scores = []
160
+
161
+ for person in filtered_candidates:
162
+ candidate_info = {
163
+ "Name": person.get("Name"),
164
+ "Roles": person.get("Roles"),
165
+ "Skills": person.get("Skills")
166
+ }
167
+ candidate_str = json.dumps(candidate_info)
168
+
169
+ score = score_candidate(candidate_str, category_name, tuple(job_titles))
170
+ scores.append(score)
171
+
172
+ df_filtered["LLM_Score"] = scores
173
+
174
+ # Only filter out scores of 0 if the token is present (0 means total irrelevance if token works)
175
+ if HF_API_TOKEN:
176
+ df_recommended = df_filtered[df_filtered["LLM_Score"] > 0].copy()
177
+ else:
178
+ df_recommended = df_filtered.copy() # Can't filter if all are 0 due to no token
179
 
180
  if df_recommended.empty:
181
+ if not HF_API_TOKEN:
182
+ return "❌ LLM failed: The HF_API_TOKEN is not set or is invalid. Set the token and try again."
183
+ return f"LLM scored all candidates 0. This indicates zero semantic relevance between the candidates' roles and the target roles for '{category_name}'."
184
 
 
185
  def parse_salary(s):
186
  try:
187
  return float(str(s).replace("$","").replace(",","").replace("N/A", str(float('inf'))))
 
190
 
191
  df_recommended["Salary_sort"] = df_recommended["Salary"].apply(parse_salary)
192
 
 
193
  df_top5 = df_recommended.sort_values(
194
+ by=['LLM_Score', 'Salary_sort'],
195
  ascending=[False, True]
196
  ).head(5)
197
 
 
200
  output_text = f"Top {len(final_names)} Recommended Candidates for the '{category_name}' Category:\n\n"
201
 
202
  for i, name in enumerate(final_names):
203
+ score = df_top5.iloc[i]['LLM_Score']
204
+ output_text += f"{i+1}. {name} (Semantic Role Match Score: {score}/10)\n"
 
 
205
 
206
+ output_text += "\nThese candidates were ranked by the LLM based on the **conceptual fit (semantic similarity)** of their previous job roles to the target roles, using expected salary as a tie-breaker."
207
 
208
  return output_text
209
 
 
224
  # Gradio interface (Updated Heading and Launch)
225
  # ----------------------------
226
  with gr.Blocks() as app:
227
+ gr.Markdown("# 🏆 Candidate Selection (Semantic Role Matching)")
228
+ gr.Markdown("### **Uses a large instruction model to score conceptual fit and similarity between roles.**")
229
 
230
  gr.Markdown("#### 🔍 Raw JSON Preview: First 5 Candidates")
231
  gr.Dataframe(show_first_candidates(), label="First 5 JSON Entries")
 
241
 
242
  gr.Markdown("---")
243
 
244
+ # Step 2: LLM Recommendations
245
+ recommend_button = gr.Button("3. Rank Candidates by Semantic Role Match")
246
+ recommend_output_text = gr.Textbox(label="Top Candidate Recommendations Summary", lines=10, placeholder="Click 'Rank Candidates by Semantic Role Match' after Step 2 completes.")
247
+ recommend_button.click(llm_recommendations, inputs=[category_dropdown], outputs=[recommend_output_text])
 
248
 
249
  if __name__ == "__main__":
250
  app.launch(share=True)