Spaces:
Sleeping
Sleeping
| """Explainability engine: Generates human-readable justifications for matches.""" | |
| from dataclasses import dataclass | |
| from typing import Callable | |
| import math | |
| class ExplainabilityBreakdown: | |
| """Human-readable breakdown of a match decision.""" | |
| overall_score: float | |
| interpretation: str # "Strong Match" / "Moderate Match" / "Weak Match" | |
| matching_skills: list[str] | |
| missing_skills: list[str] | |
| experience_alignment: str | |
| key_reason: str | |
| recommendations: list[str] | |
| def generate_explanation( | |
| candidate_name: str, | |
| job_title: str, | |
| match_score: dict, | |
| matching_skills: list[str], | |
| missing_skills: list[str], | |
| candidate_years_exp: float = 0.0, | |
| required_years_exp: float = 0.0, | |
| ) -> ExplainabilityBreakdown: | |
| """ | |
| Generate human-readable explanation for a CV-job match. | |
| Args: | |
| candidate_name: Candidate full name | |
| job_title: Job title | |
| match_score: Dict with 'overall', 'text_sim', 'skills_match' scores (0-1) | |
| matching_skills: List of skills that matched | |
| missing_skills: List of required skills not found | |
| candidate_years_exp: Candidate's years of experience | |
| required_years_exp: Required years for job | |
| Returns: | |
| ExplainabilityBreakdown with justification details | |
| """ | |
| overall = match_score.get("match_score", 0.0) | |
| text_sim = match_score.get("text_similarity", 0.0) | |
| skill_match = match_score.get("skills_match", 0.0) | |
| # Interpretation | |
| if overall >= 0.80: | |
| interpretation = "π’ Strong Match" | |
| elif overall >= 0.60: | |
| interpretation = "π‘ Moderate Match" | |
| else: | |
| interpretation = "π΄ Weak Match" | |
| # Experience alignment | |
| exp_gap = candidate_years_exp - required_years_exp | |
| if exp_gap >= 2: | |
| experience_alignment = f"β Highly experienced ({candidate_years_exp:.1f} years > {required_years_exp:.1f} required)" | |
| elif exp_gap >= 0: | |
| experience_alignment = f"β Meets experience requirement ({candidate_years_exp:.1f} years β {required_years_exp:.1f} required)" | |
| elif exp_gap > -2: | |
| experience_alignment = f"β οΈ Slightly under-experienced ({candidate_years_exp:.1f} years < {required_years_exp:.1f} required)" | |
| else: | |
| experience_alignment = f"β Under-experienced ({candidate_years_exp:.1f} years < {required_years_exp:.1f} required)" | |
| # Key reason | |
| if len(matching_skills) >= 5 and skill_match > 0.85: | |
| key_reason = f"Strong technical skills alignment: {', '.join(matching_skills[:3])} + {len(matching_skills)-3} more" | |
| elif len(matching_skills) > 0: | |
| key_reason = f"Good skills match: {', '.join(matching_skills[:2])}" | |
| elif text_sim > 0.75: | |
| key_reason = "Strong text/profile similarity with job description" | |
| else: | |
| key_reason = "Limited overlap between background and job requirements" | |
| # Recommendations | |
| recommendations = [] | |
| if len(missing_skills) > 0: | |
| recommendations.append(f"Consider candidates with: {', '.join(missing_skills[:2])}") | |
| if exp_gap < -1: | |
| recommendations.append("Candidate needs more industry experience") | |
| if overall > 0.60: | |
| recommendations.append("Recommended for interview or technical assessment") | |
| if overall < 0.50: | |
| recommendations.append("Better candidates may exist; consider re-posting") | |
| if not recommendations: | |
| recommendations.append("Strong candidate β proceed with hiring process") | |
| return ExplainabilityBreakdown( | |
| overall_score=round(overall, 2), | |
| interpretation=interpretation, | |
| matching_skills=matching_skills[:5], # Top 5 | |
| missing_skills=missing_skills[:3], # Top 3 | |
| experience_alignment=experience_alignment, | |
| key_reason=key_reason, | |
| recommendations=recommendations[:2], # Top 2 recommendations | |
| ) | |
| def generate_shortlist_summary( | |
| matches: list[dict], | |
| job_title: str, | |
| top_n: int = 5, | |
| ) -> dict: | |
| """ | |
| Generate summary for a shortlist of candidates. | |
| Args: | |
| matches: List of match results (each with candidate, score, skills data) | |
| job_title: Job title being matched | |
| top_n: Number of top candidates to feature | |
| Returns: | |
| Summary dict with insights and recommendations | |
| """ | |
| if not matches: | |
| return { | |
| "total_candidates_screened": 0, | |
| "strong_matches": 0, | |
| "recommendations": ["No candidates found. Consider widening search criteria."], | |
| "top_skills_in_shortlist": [], | |
| } | |
| sorted_matches = sorted(matches, key=lambda x: x.get("score", 0), reverse=True) | |
| top_matches = sorted_matches[:top_n] | |
| strong = sum(1 for m in matches if m.get("score", 0) >= 0.80) | |
| moderate = sum(1 for m in matches if 0.60 <= m.get("score", 0) < 0.80) | |
| # Extract top skills from candidates | |
| all_skills = [] | |
| for match in top_matches: | |
| if "matching_skills" in match: | |
| all_skills.extend(match["matching_skills"]) | |
| from collections import Counter | |
| skill_counts = Counter(all_skills) | |
| top_skills = [skill for skill, count in skill_counts.most_common(5)] | |
| recommendations = [] | |
| if strong >= 3: | |
| recommendations.append(f"β Excellent pool: {strong} strong matches. Recommend interviews for all.") | |
| elif strong >= 1: | |
| recommendations.append(f"π Good: {strong} strong match(es) available. Start with top candidates.") | |
| else: | |
| recommendations.append(f"β οΈ Limited strong matches ({strong}). Consider adjusting requirements or widening search.") | |
| if moderate > 0: | |
| recommendations.append(f"Consider {moderate} moderate matches for technical screening.") | |
| return { | |
| "total_candidates_screened": len(matches), | |
| "strong_matches": strong, | |
| "moderate_matches": moderate, | |
| "top_skills_in_pool": top_skills, | |
| "recommendations": recommendations, | |
| } | |