MrugajaJ commited on
Commit
1e37007
·
verified ·
1 Parent(s): 57e5254

Delete src/app.py

Browse files
Files changed (1) hide show
  1. src/app.py +0 -354
src/app.py DELETED
@@ -1,354 +0,0 @@
1
- import streamlit as st
2
- import json
3
- import time
4
- from pathlib import Path
5
- import pandas as pd
6
- import numpy as np
7
- from sentence_transformers import SentenceTransformer
8
- from sklearn.feature_extraction.text import TfidfVectorizer
9
-
10
- from src.hard_filter import is_killed
11
- from src.score_career import compute_A, compute_keyword_max
12
- from src.score_skills import compute_B
13
- from src.score_embed import compute_C_all
14
- from src.availability import apply_multipliers
15
- from src.output import write_submission
16
- from src.precompute import build_candidate_text, build_jd_text
17
-
18
- # Set Page Config
19
- st.set_page_config(
20
- page_title="Vettly Talent Intelligence Portal",
21
- page_icon="💼",
22
- layout="wide",
23
- initial_sidebar_state="expanded"
24
- )
25
-
26
- # Custom HR-Themed Styling
27
- st.markdown("""
28
- <style>
29
- /* Warm Ivory & Deep Indigo Theme */
30
- .reportview-container {
31
- background-color: #FAF8F5;
32
- }
33
- .sidebar .sidebar-content {
34
- background-color: #1E1B4B;
35
- color: #FFFFFF;
36
- }
37
-
38
- /* Elegant Header */
39
- .title-container {
40
- padding: 2rem;
41
- background: linear-gradient(135deg, #1E1B4B 0%, #312E81 100%);
42
- color: white;
43
- border-radius: 12px;
44
- margin-bottom: 2rem;
45
- box-shadow: 0 4px 15px rgba(0,0,0,0.05);
46
- }
47
-
48
- /* Candidate Cards */
49
- .candidate-card {
50
- background-color: #FFFFFF;
51
- border: 1px solid #E2E8F0;
52
- border-left: 5px solid #F43F5E; /* Rose accent */
53
- padding: 1.5rem;
54
- border-radius: 8px;
55
- margin-bottom: 1.2rem;
56
- box-shadow: 0 2px 4px rgba(0,0,0,0.02);
57
- transition: transform 0.2s ease, box-shadow 0.2s ease;
58
- }
59
- .candidate-card:hover {
60
- transform: translateY(-2px);
61
- box-shadow: 0 6px 12px rgba(0,0,0,0.05);
62
- }
63
-
64
- /* Badges & Metrics */
65
- .metric-badge {
66
- display: inline-block;
67
- padding: 0.25rem 0.6rem;
68
- border-radius: 9999px;
69
- font-size: 0.8rem;
70
- font-weight: 600;
71
- margin-right: 0.5rem;
72
- }
73
- .badge-rose {
74
- background-color: #FFE4E6;
75
- color: #E11D48;
76
- }
77
- .badge-indigo {
78
- background-color: #E0E7FF;
79
- color: #4F46E5;
80
- }
81
- .badge-emerald {
82
- background-color: #D1FAE5;
83
- color: #059669;
84
- }
85
-
86
- /* Stats Layout */
87
- .stat-box {
88
- background-color: #FFFFFF;
89
- border: 1px solid #E2E8F0;
90
- padding: 1rem;
91
- border-radius: 8px;
92
- text-align: center;
93
- box-shadow: 0 1px 3px rgba(0,0,0,0.01);
94
- }
95
-
96
- /* Typography adjustments */
97
- h1, h2, h3 {
98
- color: #1E1B4B !important;
99
- font-family: 'Inter', sans-serif;
100
- }
101
- </style>
102
- """, unsafe_allow_html=True)
103
-
104
- # App Title & Welcome Banner
105
- st.markdown("""
106
- <div class="title-container">
107
- <h1 style="color: white !important; margin:0; font-size: 2.2rem; font-weight:700;">Vettly Talent Portal</h1>
108
- <p style="margin: 0.5rem 0 0 0; opacity: 0.9; font-size:1.1rem;">AI-Assisted Candidate Discovery, Fit Analysis, and Talent Alignment</p>
109
- </div>
110
- """, unsafe_allow_html=True)
111
-
112
- # App Title & Welcome Banner
113
- st.markdown("""
114
- <div class="title-container">
115
- <h1 style="color: white !important; margin:0; font-size: 2.2rem; font-weight:700;">Vettly Talent Portal</h1>
116
- <p style="margin: 0.5rem 0 0 0; opacity: 0.9; font-size:1.1rem;">AI-Assisted Candidate Discovery, Fit Analysis, and Talent Alignment</p>
117
- </div>
118
- """, unsafe_allow_html=True)
119
-
120
- # Sidebar Setup
121
- with st.sidebar:
122
- st.markdown("<h3 style='color: white !important; margin-bottom: 1.5rem;'>📋 Upload Datasets</h3>", unsafe_allow_html=True)
123
-
124
- # 1. Job Description Upload
125
- uploaded_jd = st.file_uploader("Upload Job Description (JSON)", type=["json"])
126
-
127
- # 2. Candidates Dataset Upload
128
- uploaded_candidates = st.file_uploader("Upload Candidates Dataset (JSONL)", type=["jsonl"])
129
-
130
- st.markdown("<hr style='border-color: #312E81;'>", unsafe_allow_html=True)
131
- st.markdown("<h4 style='color: white !important;'>Score Weights</h4>", unsafe_allow_html=True)
132
- w_A = st.slider("Career Fit Weight (A)", 0.0, 1.0, 0.40, 0.05)
133
- w_B = st.slider("Skill Trust Weight (B)", 0.0, 1.0, 0.35, 0.05)
134
- w_C = st.slider("Semantic Similarity Weight (C)", 0.0, 1.0, 0.25, 0.05)
135
-
136
- # Check normalization
137
- if abs((w_A + w_B + w_C) - 1.0) > 0.001:
138
- st.warning(f"Weights sum to {w_A+w_B+w_C:.2f}. They will be normalized to 1.0 internally.")
139
-
140
- # Check if inputs are uploaded
141
- if not uploaded_jd or not uploaded_candidates:
142
- st.info("👋 Welcome! Please upload both the **Job Description (JSON)** and **Candidates Dataset (JSONL)** in the sidebar to begin.")
143
- else:
144
- jd_data = json.load(uploaded_jd)
145
-
146
- # Display JD summary info
147
- col1, col2 = st.columns([1, 2])
148
- with col1:
149
- st.markdown("### Job Specifications")
150
- st.write(f"**Target Role:** {jd_data.get('title', 'Unknown')}")
151
- st.write(f"**Required Experience:** {jd_data.get('min_yoe', 5)}+ years")
152
- st.write(f"**Max Budget:** {jd_data.get('budget_max_inr_lpa', 'N/A')} LPA")
153
- st.write(f"**Preferred Location(s):** {', '.join(jd_data.get('preferred_locations', []))}")
154
-
155
- with col2:
156
- st.markdown("### Focus Skills & Keywords")
157
- must_haves = jd_data.get("must_have_skills", [])
158
- st.markdown("**Must Have Skills:**")
159
- st.write(", ".join([f"`{s}`" for s in must_haves]))
160
-
161
- kws = jd_data.get("keywords", [])
162
- st.markdown("**Target Keywords:**")
163
- st.write(", ".join([f"`{k}`" for k in kws]))
164
-
165
- # Start Button
166
- if st.button("🚀 Start Talent Search & Vetting Pipeline", use_container_width=True):
167
- # Normalize weights
168
- total_w = w_A + w_B + w_C
169
- nw_A, nw_B, nw_C = w_A/total_w, w_B/total_w, w_C/total_w
170
-
171
- status_box = st.empty()
172
- progress_bar = st.progress(0)
173
-
174
- # 1. Loading & Streaming from memory buffer
175
- status_box.info("Streaming uploaded candidates & fitting TF-IDF parameters...")
176
- progress_bar.progress(15)
177
-
178
- titles = []
179
- # Reset and read lines from the uploaded file buffer
180
- uploaded_candidates.seek(0)
181
- for line_bytes in uploaded_candidates:
182
- line = line_bytes.decode("utf-8").strip()
183
- if not line:
184
- continue
185
- cand = json.loads(line)
186
- title = cand.get("profile", {}).get("current_title") or cand.get("current_title") or ""
187
- titles.append(title)
188
-
189
- tfidf = TfidfVectorizer(max_features=30000, ngram_range=(1, 2))
190
- tfidf.fit(titles)
191
- del titles
192
-
193
- # Reset and read lines for Filtering Pass
194
- uploaded_candidates.seek(0)
195
-
196
-
197
- # 2. Hard Filtering
198
- status_box.info("Applying hard gatekeeper rules (Profile Completeness, Activity, Intent)...")
199
- progress_bar.progress(35)
200
-
201
- survivors = []
202
- killed_reasons = {}
203
- for line_bytes in uploaded_candidates:
204
- line = line_bytes.decode("utf-8").strip()
205
- if not line:
206
- continue
207
- cand = json.loads(line)
208
- killed_flag, reason = is_killed(cand, jd_data, tfidf)
209
- if killed_flag:
210
- # Categorize reason for display
211
- category = "Other Filter"
212
- if "profile_completeness_score" in reason:
213
- category = "Incomplete Profile"
214
- elif "verified_email" in reason:
215
- category = "Unverified Email"
216
- elif "interview_completion_rate" in reason:
217
- category = "Low Interview Completion"
218
- elif "Inactive" in reason or "last_active_date" in reason:
219
- category = "Inactive > 180 Days"
220
- elif "open_to_work_flag" in reason:
221
- category = "Not Open to Work"
222
- elif "Zero industry overlap" in reason:
223
- category = "Industry Mismatch"
224
- elif "Title similarity" in reason:
225
- category = "Role/Title Mismatch"
226
- killed_reasons[category] = killed_reasons.get(category, 0) + 1
227
- else:
228
- survivors.append(cand)
229
-
230
- # 3. Embedding Matching
231
- status_box.info(f"Generating semantic candidate vectors for {len(survivors)} surviving profiles...")
232
- progress_bar.progress(60)
233
-
234
- model = SentenceTransformer("all-MiniLM-L6-v2")
235
- jd_text = build_jd_text(jd_data)
236
- jd_vec = model.encode(jd_text, normalize_embeddings=True).astype("float32")
237
-
238
- survivor_texts = [build_candidate_text(s) for s in survivors]
239
- cand_vecs = model.encode(survivor_texts, batch_size=256, normalize_embeddings=True).astype("float32")
240
-
241
- # 4. Scoring
242
- status_box.info("Calculating comprehensive fit scores & multipliers...")
243
- progress_bar.progress(85)
244
-
245
- C_scores = compute_C_all(jd_vec, cand_vecs)
246
- C_map = {str(s.get("candidate_id") or s.get("id")): float(score) for s, score in zip(survivors, C_scores)}
247
-
248
- # Fit survivors TF-IDF
249
- tfidf_surv = TfidfVectorizer(max_features=30000, ngram_range=(1, 2))
250
- tfidf_surv.fit(survivor_texts)
251
- keyword_max = compute_keyword_max(survivors, jd_data, tfidf_surv)
252
-
253
- raw_scored = []
254
- for cand in survivors:
255
- cand_id = str(cand.get("candidate_id") or cand.get("id"))
256
- A_res = compute_A(cand, jd_data, tfidf_surv, keyword_max)
257
- B_res = compute_B(cand, jd_data)
258
- C = C_map.get(cand_id, 0.0)
259
-
260
- A = A_res["A"]
261
- B = B_res["B"]
262
- raw_score = round(nw_A * A + nw_B * B + nw_C * C, 4)
263
-
264
- raw_scored.append({
265
- "candidate_id": cand_id,
266
- "candidate": cand,
267
- "A": A,
268
- "B": B,
269
- "C": C,
270
- "raw_score": raw_score
271
- })
272
-
273
- final_scored = apply_multipliers(raw_scored, jd_data)
274
-
275
- # Sort and take Top 50 for display
276
- final_scored = sorted(final_scored, key=lambda x: x["final_score"], reverse=True)
277
- top_candidates = final_scored[:50]
278
-
279
- # Clear status
280
- status_box.empty()
281
- progress_bar.empty()
282
-
283
- # Display Stats Summary Dashboard
284
- st.markdown("### Talent Pipeline Summary Dashboard")
285
- d_col1, d_col2, d_col3, d_col4 = st.columns(4)
286
- with d_col1:
287
- st.markdown(f"<div class='stat-box'><h4>Total Profiles</h4><h2 style='color:#1E1B4B;'>100,000</h2></div>", unsafe_allow_html=True)
288
- with d_col2:
289
- st.markdown(f"<div class='stat-box'><h4>Filtered Out</h4><h2 style='color:#E11D48;'>{100000 - len(survivors):,}</h2></div>", unsafe_allow_html=True)
290
- with d_col3:
291
- st.markdown(f"<div class='stat-box'><h4>Qualified Survivors</h4><h2 style='color:#059669;'>{len(survivors):,}</h2></div>", unsafe_allow_html=True)
292
- with d_col4:
293
- st.markdown(f"<div class='stat-box'><h4>Pruned Ratio</h4><h2 style='color:#D97706;'>{((100000 - len(survivors))/100000)*100:.2f}%</h2></div>", unsafe_allow_html=True)
294
-
295
- # Draw Bar chart of filtering reasons
296
- st.markdown("#### Primary Reasons for Candidate Disqualification")
297
- df_reasons = pd.DataFrame(list(killed_reasons.items()), columns=["Disqualification Category", "Candidate Count"])
298
- st.bar_chart(df_reasons.set_index("Disqualification Category"), color="#F43F5E")
299
-
300
- # Output Top Candidates list in a gorgeous card design
301
- st.markdown("### 🏆 Top 50 Matched Candidates")
302
-
303
- for rank, cand_item in enumerate(top_candidates, 1):
304
- cand = cand_item["candidate"]
305
- profile = cand.get("profile") or {}
306
- anom_name = profile.get("anonymized_name", "Anonymous Candidate")
307
- curr_title = profile.get("current_title", "Software Professional")
308
- curr_company = profile.get("current_company", "N/A")
309
- yoe = profile.get("years_of_experience") or profile.get("yoe") or 0.0
310
- loc = profile.get("location", "Remote")
311
-
312
- # Scores
313
- final_pct = int(cand_item["final_score"] * 100)
314
- score_A_pct = int(cand_item["A"] * 100)
315
- score_B_pct = int(cand_item["B"] * 100)
316
- score_C_pct = int(cand_item["C"] * 100)
317
-
318
- # HTML Card block
319
- st.markdown(f"""
320
- <div class="candidate-card">
321
- <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
322
- <div>
323
- <span class="metric-badge badge-rose" style="font-size:1rem; padding: 0.4rem 0.8rem;">Rank #{rank}</span>
324
- <strong style="font-size:1.2rem; color:#1E1B4B; margin-left: 0.5rem;">{anom_name}</strong>
325
- <span style="color:#64748B; margin-left:1rem;">{curr_title} @ {curr_company}</span>
326
- </div>
327
- <div>
328
- <span style="font-size:1.6rem; font-weight:700; color:#E11D48;">{final_pct}% Match</span>
329
- </div>
330
- </div>
331
- <div style="margin-bottom: 0.8rem;">
332
- <span class="metric-badge badge-indigo">💼 {yoe} Years Experience</span>
333
- <span class="metric-badge badge-indigo">📍 {loc}</span>
334
- <span class="metric-badge badge-emerald">Career Fit: {score_A_pct}%</span>
335
- <span class="metric-badge badge-emerald">Skills Trust: {score_B_pct}%</span>
336
- <span class="metric-badge badge-emerald">Semantic Sim: {score_C_pct}%</span>
337
- </div>
338
- </div>
339
- """, unsafe_allow_html=True)
340
-
341
- # Details Expander
342
- with st.expander(f"Inspect profile details & hiring alignment for {anom_name}"):
343
- st.markdown("**Core Fit Analysis:**")
344
- st.write(f"✅ Candidate has a match score of {final_pct}%. They possess {yoe} years of relevant industry experience in {profile.get('current_industry', 'tech')}. Matched locations include {loc}.")
345
-
346
- # Show career history
347
- st.markdown("**Career History Summary:**")
348
- for job in cand.get("career_history", []):
349
- st.write(f"- **{job.get('title')}** at *{job.get('company')}* ({job.get('duration_months', 0)} months) — *{job.get('description', '')[:200]}...*")
350
-
351
- # Show skills
352
- st.markdown("**Technical Skills Inventory:**")
353
- skills_list = [s.get("name") if isinstance(s, dict) else s for s in cand.get("skills", [])]
354
- st.write(", ".join([f"`{s}`" for s in skills_list[:15]]))