Spaces:
Running
Running
| EDUCATION_LEVELS = { | |
| "Unknown": 0, "High School": 1, "Bachelor": 2, "Master": 3, "PhD": 4 | |
| } | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Individual component scoring helpers | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def calculate_experience_score(required_exp: int, candidate_exp: float) -> float: | |
| if required_exp <= 0: | |
| return 100.0 | |
| if candidate_exp >= required_exp: | |
| return 100.0 | |
| return round((candidate_exp / required_exp) * 100.0, 2) | |
| def calculate_location_score(preferred_location: str, candidate_location: str) -> float: | |
| if not preferred_location: | |
| return 100.0 | |
| if not candidate_location: | |
| return 50.0 | |
| pref_clean = preferred_location.lower().strip() | |
| cand_clean = candidate_location.lower().strip() | |
| if pref_clean in cand_clean or cand_clean in pref_clean: | |
| return 100.0 | |
| if "remote" in pref_clean or "remote" in cand_clean: | |
| return 80.0 | |
| return 0.0 | |
| def calculate_education_score(required_education: str, candidate_education: str) -> float: | |
| if not required_education or required_education == "Any": | |
| return 100.0 | |
| req_level = EDUCATION_LEVELS.get(required_education, 0) | |
| cand_level = EDUCATION_LEVELS.get(candidate_education, 0) | |
| if cand_level >= req_level: | |
| return 100.0 | |
| if req_level == 0: | |
| return 100.0 | |
| return round((cand_level / req_level) * 100.0, 2) | |
| def calculate_certification_score(required_certs: list, candidate_certs: list) -> float: | |
| if not required_certs: | |
| return 100.0 | |
| if not candidate_certs: | |
| return 0.0 | |
| req_lower = [c.lower() for c in required_certs] | |
| cand_lower = [c.lower() for c in candidate_certs] | |
| matched = sum(1 for c in req_lower if any(c in cc or cc in c for cc in cand_lower)) | |
| return round((matched / len(required_certs)) * 100.0, 2) | |
| def calculate_language_score(preferred_languages: list, candidate_languages: list) -> float: | |
| if not preferred_languages: | |
| return 100.0 | |
| if not candidate_languages: | |
| return 50.0 | |
| pref_lower = [l.lower() for l in preferred_languages] | |
| cand_lower = [l.lower() for l in candidate_languages] | |
| matched = sum(1 for l in pref_lower if l in cand_lower) | |
| return round((matched / len(preferred_languages)) * 100.0, 2) | |
| def calculate_skills_score(required_skills: list, candidate_skills: list) -> float: | |
| """Exact-match skills score.""" | |
| if not required_skills: | |
| return 100.0 | |
| if not candidate_skills: | |
| return 0.0 | |
| req_lower = set(s.lower() for s in required_skills) | |
| cand_lower = set(s.lower() for s in candidate_skills) | |
| matched = len(req_lower & cand_lower) | |
| return round((matched / len(req_lower)) * 100.0, 2) | |
| def calculate_density_weighted_skills_score(required_skills: list, skills_density: dict) -> float: | |
| """ | |
| Density-aware skills score. Penalises single-mention keyword stuffing. | |
| count == 1 β 0.4 (casual / stuffed mention) | |
| count == 2 β 0.7 (moderate use) | |
| count >= 3 β 1.0 (core expertise) | |
| """ | |
| if not required_skills: | |
| return 100.0 | |
| if not skills_density: | |
| return 0.0 | |
| density_lower = {k.lower(): v for k, v in skills_density.items()} | |
| total_weight = 0.0 | |
| for skill in required_skills: | |
| count = density_lower.get(skill.lower(), 0) | |
| if count == 0: | |
| multiplier = 0.0 | |
| elif count == 1: | |
| multiplier = 0.4 | |
| elif count == 2: | |
| multiplier = 0.7 | |
| else: | |
| multiplier = 1.0 | |
| total_weight += multiplier | |
| return round((total_weight / len(required_skills)) * 100.0, 2) | |
| def calculate_projects_score(candidate_projects: list) -> float: | |
| if not candidate_projects: | |
| return 0.0 | |
| count = len(candidate_projects) | |
| if count >= 3: | |
| return 100.0 | |
| return round((count / 3) * 100.0, 2) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Utility helpers | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_matched_missing_skills(required_skills: list, candidate_skills: list): | |
| req_lower = {s.lower(): s for s in required_skills} | |
| cand_lower = set(s.lower() for s in candidate_skills) | |
| matched = [req_lower[r] for r in req_lower if r in cand_lower] | |
| missing = [req_lower[r] for r in req_lower if r not in cand_lower] | |
| return matched, missing | |
| def check_seniority_deficit(job_title: str, past_titles: list) -> bool: | |
| """ | |
| Checks if a candidate is applying for a senior/lead job while their history | |
| consists entirely of junior-level titles and contains no mid-level or senior titles. | |
| """ | |
| if not job_title or not past_titles: | |
| return False | |
| senior_keywords = {"senior", "lead", "principal", "manager", "architect", "director", "head", "vp", "specialist"} | |
| junior_modifiers = {"junior", "associate", "intern", "graduate", "trainee", "entry", "entry-level"} | |
| job_lower = job_title.lower() | |
| is_senior_job = any(k in job_lower for k in senior_keywords) | |
| if not is_senior_job: | |
| return False | |
| # Check if they have ANY senior history | |
| has_any_senior_history = any(any(k in t.lower() for k in senior_keywords) for t in past_titles) | |
| if has_any_senior_history: | |
| return False | |
| # Check if they have ANY mid-level history (a title that has neither junior nor senior keywords) | |
| has_any_mid_level = False | |
| for title in past_titles: | |
| t_low = title.lower() | |
| is_junior = any(k in t_low for k in junior_modifiers) | |
| is_senior = any(k in t_low for k in senior_keywords) | |
| if not is_junior and not is_senior: | |
| has_any_mid_level = True | |
| break | |
| # Seniority deficit gate triggers ONLY if all extracted titles are junior/intern roles | |
| all_roles_are_junior = all(any(k in t.lower() for k in junior_modifiers) for t in past_titles) | |
| if all_roles_are_junior and not has_any_senior_history and not has_any_mid_level: | |
| return True | |
| return False | |
| def generate_summary(candidate_id: str, matched_skills: list, missing_skills: list, | |
| exp_score: float, semantic_score: float, final_score: float) -> str: | |
| strength = "strong" if final_score >= 80 else "moderate" if final_score >= 60 else "limited" | |
| skill_note = f"Matches {len(matched_skills)} required skill(s)." if matched_skills else "No direct skill matches found." | |
| gap_note = f"Gaps: {', '.join(missing_skills[:3])}." if missing_skills else "No critical skill gaps detected." | |
| exp_note = "Experience fully meets requirements." if exp_score >= 100 else f"Experience score: {exp_score:.0f}%." | |
| sem_note = f"Semantic alignment with job description: {semantic_score:.0f}%." | |
| return f"{candidate_id} shows {strength} overall fit. {skill_note} {gap_note} {exp_note} {sem_note}" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Final composite scorer β v3 "Soft Veto" with Non-Linear Calibrations | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def compute_final_score(extracted_data: dict, job_reqs: dict, semantic_score: float, | |
| skill_similarity_score: float, semantic_model=None) -> dict: | |
| """ | |
| v3 Scoring with upgraded Non-Linear experience deficit penalty and audit logs. | |
| """ | |
| weights = { | |
| "semantic": 0.40, | |
| "skills": 0.20, | |
| "experience": 0.15, | |
| "education": 0.10, | |
| "certifications": 0.05, | |
| "location": 0.05, | |
| "language": 0.05, | |
| } | |
| req_skills = job_reqs.get("required_skills", []) | |
| cand_skills = extracted_data.get("skills", []) | |
| skills_density = extracted_data.get("skills_density", {}) | |
| # ββ 1. Skill sub-scores ββββββββββββββββββββββ | |
| exact_score = calculate_skills_score(req_skills, cand_skills) | |
| density_score = calculate_density_weighted_skills_score(req_skills, skills_density) | |
| sem_skill_score = skill_similarity_score | |
| # Blend: 40% exact | 30% density | 30% semantic-skills | |
| blended_skill_score = (exact_score * 0.40) + (density_score * 0.30) + (sem_skill_score * 0.30) | |
| # ββ 2. Core tech score (out of 60 possible points) ββ | |
| tech_score = (blended_skill_score * weights["skills"]) + (semantic_score * weights["semantic"]) | |
| # ββ 3. Soft Veto βββββββββββββββββββββββββββββ | |
| if tech_score >= 30.0: | |
| tech_ratio = 1.0 | |
| else: | |
| tech_ratio = max(0.4, tech_score / 30.0) | |
| # ββ 4. Secondary scores ββββββββββββββββββββββ | |
| req_exp_years = job_reqs.get("required_experience_years", 0) | |
| cand_exp_years = extracted_data.get("experience", 0) | |
| exp_score = calculate_experience_score(req_exp_years, cand_exp_years) | |
| required_education = job_reqs.get("required_education", "Any") | |
| candidate_education = extracted_data.get("education", "Unknown") | |
| edu_score = calculate_education_score(required_education, candidate_education) | |
| required_certifications = job_reqs.get("required_certifications", []) | |
| candidate_certifications = extracted_data.get("certifications", []) | |
| cert_score = calculate_certification_score(required_certifications, candidate_certifications) | |
| preferred_location = job_reqs.get("preferred_location", "") | |
| candidate_location = extracted_data.get("location", "") | |
| loc_score = calculate_location_score(preferred_location, candidate_location) | |
| preferred_languages = job_reqs.get("preferred_languages", []) | |
| candidate_languages = extracted_data.get("languages", []) | |
| lang_score = calculate_language_score(preferred_languages, candidate_languages) | |
| adjusted_exp_score = exp_score * tech_ratio | |
| adjusted_edu_score = edu_score * tech_ratio | |
| adjusted_cert_score = cert_score | |
| adjusted_loc_score = loc_score | |
| adjusted_lang_score = lang_score | |
| # ββ 5. Final composite ββββββββββββββββββββββββ | |
| final_score = ( | |
| tech_score + | |
| (adjusted_exp_score * weights["experience"]) + | |
| (adjusted_edu_score * weights["education"]) + | |
| (adjusted_cert_score * weights["certifications"]) + | |
| (adjusted_loc_score * weights["location"]) + | |
| (adjusted_lang_score * weights["language"]) | |
| ) | |
| # ββ 6. Deficit Score Penalties & Gate ββββββββββ | |
| # Seniority Deficit Gate matching | |
| job_title_input = job_reqs.get("job_title", "") | |
| past_titles = extracted_data.get("past_titles", []) | |
| seniority_deficit = check_seniority_deficit(job_title_input, past_titles) | |
| seniority_explanation = "Seniority history is aligned with requirements." | |
| if seniority_deficit: | |
| final_score = final_score * 0.75 | |
| seniority_explanation = "Applied 25% reduction: Candidate is applying for a senior/lead role but their history only contains junior/intern titles." | |
| # Experience Deficit sliding piecewise penalty | |
| exp_penalty = 0.0 | |
| exp_explanation = "Experience fully meets or exceeds requirements." | |
| if cand_exp_years < req_exp_years: | |
| exp_deficit = req_exp_years - cand_exp_years | |
| # Piecewise curve | |
| if exp_deficit <= 2.0: | |
| exp_penalty = exp_deficit * 3.0 | |
| elif exp_deficit <= 5.0: | |
| exp_penalty = 6.0 + (exp_deficit - 2.0) * 4.0 | |
| else: | |
| exp_penalty = 18.0 + (exp_deficit - 5.0) * 1.5 | |
| # Cap the maximum experience penalty | |
| exp_penalty = min(25.0, exp_penalty) | |
| final_score = final_score - exp_penalty | |
| exp_explanation = f"Deducted {exp_penalty:.1f} points: Candidate has {cand_exp_years} years, job requires {req_exp_years} years (Applied non-linear gap scaling)." | |
| final_score = max(0.0, min(100.0, final_score)) | |
| # Compile a highly detailed analytical audit log | |
| matched_skills, missing_skills = get_matched_missing_skills(req_skills, cand_skills) | |
| edu_explanation = f"Education level meets requirements (Candidate: {candidate_education}, Required: {required_education})." | |
| if edu_score < 100.0: | |
| edu_explanation = f"Education deficit: Candidate has {candidate_education}, but role requires {required_education}. Applied soft-veto scaling." | |
| skills_explanation = f"Blended skills match: {blended_skill_score:.1f}% (Composed of 40% keyword intersection, 30% frequency density, and 30% dense-context semantic skill similarity)." | |
| semantic_explanation = f"Semantic alignment with JD: {semantic_score:.1f}%. Represents overall conceptual match." | |
| loc_explanation = "Location check passed." | |
| if loc_score == 80.0: | |
| loc_explanation = "Location matched via hybrid/remote settings." | |
| elif loc_score == 0.0 and preferred_location: | |
| loc_explanation = f"Location mismatch: Role preferred {preferred_location}, candidate resides in {candidate_location or 'Unknown'}." | |
| cert_explanation = "Certification requirements met." | |
| if required_certifications and cert_score < 100.0: | |
| cert_explanation = f"Missing certifications: Candidate matched {len(candidate_certifications)} of {len(required_certifications)} required certs." | |
| audit_log = { | |
| "experience": exp_explanation, | |
| "seniority": seniority_explanation, | |
| "education": edu_explanation, | |
| "skills": skills_explanation, | |
| "semantic_similarity": semantic_explanation, | |
| "location": loc_explanation, | |
| "certifications": cert_explanation | |
| } | |
| return { | |
| "final_score": round(final_score, 2), | |
| "breakdown": { | |
| "skills": round(blended_skill_score, 2), | |
| "semantic_similarity": round(semantic_score, 2), | |
| "experience": round(exp_score, 2), | |
| "education": round(edu_score, 2), | |
| "certifications": round(cert_score, 2), | |
| "location": round(loc_score, 2), | |
| "language": round(lang_score, 2), | |
| }, | |
| "audit_log": audit_log | |
| } | |