import streamlit as st import json import time from pathlib import Path import pandas as pd import numpy as np from sentence_transformers import SentenceTransformer from sklearn.feature_extraction.text import TfidfVectorizer from src.hard_filter import is_killed from src.score_career import compute_A, compute_keyword_max from src.score_skills import compute_B from src.score_embed import compute_C_all from src.availability import apply_multipliers from src.output import write_submission from src.precompute import build_candidate_text, build_jd_text # Set Page Config st.set_page_config( page_title="Vettly Talent Portal", layout="wide", initial_sidebar_state="expanded" ) # Custom Premium Dark Theme Styling st.markdown(""" """, unsafe_allow_html=True) # App Title & Welcome Banner st.markdown("

Vettly Talent Portal

", unsafe_allow_html=True) st.markdown("

AI-Assisted Candidate Discovery, Fit Analysis, and Role Alignment

", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) # Center Uploads st.markdown("### Upload Datasets") col_up1, col_up2 = st.columns(2) with col_up1: uploaded_jd = st.file_uploader("Job Description (JSON)", type=["json"]) with col_up2: uploaded_candidates = st.file_uploader("Candidates Dataset (JSONL)", type=["jsonl"]) use_default_candidates = st.checkbox("Use Demo Candidates Dataset (100,000 Profiles) - Instant Load", value=False) if use_default_candidates: uploaded_candidates = open("candidates.jsonl", "rb") # Sidebar Setup with st.sidebar: st.markdown("### Score Weights Configuration") w_A = st.slider("Career Fit Weight (A)", 0.0, 1.0, 0.40, 0.05) w_B = st.slider("Skill Trust Weight (B)", 0.0, 1.0, 0.35, 0.05) w_C = st.slider("Semantic Similarity Weight (C)", 0.0, 1.0, 0.25, 0.05) if abs((w_A + w_B + w_C) - 1.0) > 0.001: st.warning(f"Weights sum to {w_A+w_B+w_C:.2f}. They will be normalized to 1.0 internally.") # Check if inputs are uploaded if not uploaded_jd or not uploaded_candidates: st.info("Welcome. Please upload the Job Description and the Candidates Dataset (or check the Demo Dataset box) above to begin. The start action will appear once files are loaded.") else: jd_data = json.load(uploaded_jd) # Display JD summary info col1, col2 = st.columns([1, 2]) with col1: st.markdown("### Job Specifications") st.write(f"**Target Role:** {jd_data.get('title', 'Unknown')}") st.write(f"**Required Experience:** {jd_data.get('min_yoe', 5)}+ years") st.write(f"**Max Budget:** {jd_data.get('budget_max_inr_lpa', 'N/A')} LPA") st.write(f"**Preferred Location(s):** {', '.join(jd_data.get('preferred_locations', []))}") with col2: st.markdown("### Focus Skills & Keywords") must_haves = jd_data.get("must_have_skills", []) st.markdown("**Must Have Skills:**") st.write(", ".join([f"`{s}`" for s in must_haves])) kws = jd_data.get("keywords", []) st.markdown("**Target Keywords:**") st.write(", ".join([f"`{k}`" for k in kws])) # Start Button if st.button("Start Talent Search & Vetting Pipeline", type="primary", use_container_width=True): # Normalize weights total_w = w_A + w_B + w_C nw_A, nw_B, nw_C = w_A/total_w, w_B/total_w, w_C/total_w status_box = st.empty() progress_bar = st.progress(0) # 1. Loading & Streaming from memory buffer status_box.info("Streaming uploaded candidates & fitting TF-IDF parameters...") progress_bar.progress(15) titles = [] # Reset and read lines from the uploaded file buffer uploaded_candidates.seek(0) for line_bytes in uploaded_candidates: line = line_bytes.decode("utf-8").strip() if not line: continue cand = json.loads(line) title = cand.get("profile", {}).get("current_title") or cand.get("current_title") or "" titles.append(title) tfidf = TfidfVectorizer(max_features=30000, ngram_range=(1, 2)) tfidf.fit(titles) del titles # Reset and read lines for Filtering Pass uploaded_candidates.seek(0) # 2. Hard Filtering status_box.info("Applying hard gatekeeper rules (Profile Completeness, Activity, Intent)...") progress_bar.progress(35) survivors = [] killed_reasons = {} for line_bytes in uploaded_candidates: line = line_bytes.decode("utf-8").strip() if not line: continue cand = json.loads(line) killed_flag, reason = is_killed(cand, jd_data, tfidf) if killed_flag: # Categorize reason for display category = "Other Filter" if "profile_completeness_score" in reason: category = "Incomplete Profile" elif "verified_email" in reason: category = "Unverified Email" elif "interview_completion_rate" in reason: category = "Low Interview Completion" elif "Inactive" in reason or "last_active_date" in reason: category = "Inactive > 180 Days" elif "open_to_work_flag" in reason: category = "Not Open to Work" elif "Zero industry overlap" in reason: category = "Industry Mismatch" elif "Title similarity" in reason: category = "Role/Title Mismatch" killed_reasons[category] = killed_reasons.get(category, 0) + 1 else: survivors.append(cand) # 3. Embedding Matching status_box.info(f"Generating semantic candidate vectors for {len(survivors)} surviving profiles...") progress_bar.progress(60) model = SentenceTransformer("all-MiniLM-L6-v2") jd_text = build_jd_text(jd_data) jd_vec = model.encode(jd_text, normalize_embeddings=True).astype("float32") survivor_texts = [build_candidate_text(s) for s in survivors] cand_vecs = model.encode(survivor_texts, batch_size=256, normalize_embeddings=True).astype("float32") # 4. Scoring status_box.info("Calculating comprehensive fit scores & multipliers...") progress_bar.progress(85) C_scores = compute_C_all(jd_vec, cand_vecs) C_map = {str(s.get("candidate_id") or s.get("id")): float(score) for s, score in zip(survivors, C_scores)} # Fit survivors TF-IDF tfidf_surv = TfidfVectorizer(max_features=30000, ngram_range=(1, 2)) tfidf_surv.fit(survivor_texts) keyword_max = compute_keyword_max(survivors, jd_data, tfidf_surv) raw_scored = [] for cand in survivors: cand_id = str(cand.get("candidate_id") or cand.get("id")) A_res = compute_A(cand, jd_data, tfidf_surv, keyword_max) B_res = compute_B(cand, jd_data) C = C_map.get(cand_id, 0.0) A = A_res["A"] B = B_res["B"] raw_score = round(nw_A * A + nw_B * B + nw_C * C, 4) raw_scored.append({ "candidate_id": cand_id, "candidate": cand, "A": A, "B": B, "C": C, "raw_score": raw_score }) final_scored = apply_multipliers(raw_scored, jd_data) # Sort and take Top 50 for display final_scored = sorted(final_scored, key=lambda x: x["final_score"], reverse=True) top_candidates = final_scored[:50] # Clear status status_box.empty() progress_bar.empty() # Display Stats Summary Dashboard st.markdown("
", unsafe_allow_html=True) st.markdown("### Talent Pipeline Summary Dashboard") d_col1, d_col2, d_col3, d_col4 = st.columns(4) with d_col1: st.markdown(f"

Total Profiles

100,000

", unsafe_allow_html=True) with d_col2: st.markdown(f"

Filtered Out

{100000 - len(survivors):,}

", unsafe_allow_html=True) with d_col3: st.markdown(f"

Qualified Survivors

{len(survivors):,}

", unsafe_allow_html=True) with d_col4: st.markdown(f"

Pruned Ratio

{((100000 - len(survivors))/100000)*100:.2f}%

", unsafe_allow_html=True) # Draw Bar chart of filtering reasons st.markdown("

Primary Reasons for Candidate Disqualification

", unsafe_allow_html=True) df_reasons = pd.DataFrame(list(killed_reasons.items()), columns=["Disqualification Category", "Candidate Count"]) st.bar_chart(df_reasons.set_index("Disqualification Category"), color="#ff7b00") st.markdown("
", unsafe_allow_html=True) # Output Top Candidates list in a gorgeous card design st.markdown("### Top 50 Matched Candidates") for rank, cand_item in enumerate(top_candidates, 1): cand = cand_item["candidate"] profile = cand.get("profile") or {} anom_name = profile.get("anonymized_name", "Anonymous Candidate") curr_title = profile.get("current_title", "Software Professional") curr_company = profile.get("current_company", "N/A") yoe = profile.get("years_of_experience") or profile.get("yoe") or 0.0 loc = profile.get("location", "Remote") # Scores final_pct = int(cand_item["final_score"] * 100) score_A_pct = int(cand_item["A"] * 100) score_B_pct = int(cand_item["B"] * 100) score_C_pct = int(cand_item["C"] * 100) # HTML Card block st.markdown(f"""
Rank #{rank} {anom_name} {curr_title} @ {curr_company}
{final_pct}% Match
Exp: {yoe} Yrs Loc: {loc} Career Fit: {score_A_pct}% Skills Trust: {score_B_pct}% Semantic Sim: {score_C_pct}%
""", unsafe_allow_html=True) # Details Expander with st.expander(f"Inspect Profile Details & Alignment: {anom_name}"): st.markdown("**Core Fit Analysis:**") 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}.") # Show career history st.markdown("**Career History Summary:**") for job in cand.get("career_history", []): st.write(f"- **{job.get('title')}** at *{job.get('company')}* ({job.get('duration_months', 0)} months) — *{job.get('description', '')[:200]}...*") # Show skills st.markdown("**Technical Skills Inventory:**") skills_list = [s.get("name") if isinstance(s, dict) else s for s in cand.get("skills", [])] st.write(", ".join([f"`{s}`" for s in skills_list[:15]]))