Spaces:
Sleeping
Sleeping
| # %%writefile core/scoring.py | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from typing import Dict, Any, List | |
| def _safe_len(x) -> int: | |
| return len(x) if isinstance(x, list) else 0 | |
| def normalize_weights(weights: Dict[str, Any]) -> Dict[str, float]: | |
| w_must = float(weights.get("must_have", 0)) | |
| w_nice = float(weights.get("nice_to_have", 0)) | |
| w_exp = float(weights.get("experience", 0)) | |
| w_soft = float(weights.get("soft_skills", 0)) | |
| total = w_must + w_nice + w_exp + w_soft | |
| if total <= 0: | |
| return {"must_have": 0.6, "nice_to_have": 0.25, "experience": 0.15, "soft_skills": 0.0} | |
| return { | |
| "must_have": w_must / total, | |
| "nice_to_have": w_nice / total, | |
| "experience": w_exp / total, | |
| "soft_skills": w_soft / total | |
| } | |
| def compute_coverage(matched: List[Any], missing: List[Any], partial: List[Any] | None = None) -> float: | |
| m = _safe_len(matched) | |
| miss = _safe_len(missing) | |
| p = _safe_len(partial) if partial is not None else 0 | |
| denom = m + miss + p | |
| if denom == 0: | |
| return 0.0 | |
| return (m + 0.5 * p) / denom | |
| def experience_score(assessment: str) -> float: | |
| assessment = (assessment or "").strip().lower() | |
| if assessment == "below": | |
| return 0.0 | |
| if assessment in ("meets", "exceeds"): | |
| return 1.0 | |
| return 0.0 | |
| def clamp(x: float, lo: float, hi: float) -> float: | |
| return max(lo, min(hi, x)) | |
| def score_candidate(jd_rubric: Dict[str, Any], match_summary: Dict[str, Any]) -> Dict[str, Any]: | |
| weights = normalize_weights(jd_rubric.get("recommended_weights", {})) | |
| mh = match_summary.get("must_have_match", {}) or {} | |
| nh = match_summary.get("nice_to_have_match", {}) or {} | |
| exp = match_summary.get("experience_analysis", {}) or {} | |
| must_cov = compute_coverage( | |
| matched=mh.get("matched", []), | |
| missing=mh.get("missing", []), | |
| partial=mh.get("partial", []) | |
| ) | |
| nice_cov = compute_coverage( | |
| matched=nh.get("matched", []), | |
| missing=nh.get("missing", []), | |
| partial=None | |
| ) | |
| exp_sc = experience_score(exp.get("assessment", "")) | |
| # Soft skills scoring (optional). Keep 0 for student MVP unless you add logic later. | |
| soft_sc = 0.0 | |
| base_total = ( | |
| weights["must_have"] * must_cov + | |
| weights["nice_to_have"] * nice_cov + | |
| weights["experience"] * exp_sc + | |
| weights["soft_skills"] * soft_sc | |
| ) * 100.0 | |
| # ✅ Bonus/Penalty based on Step-4 indicators (transparent & capped) | |
| positives = match_summary.get("positive_indicators", []) or [] | |
| negatives = match_summary.get("negative_indicators", []) or [] | |
| bonus_per_positive = 1.0 | |
| penalty_per_negative = 1.5 | |
| raw_adjustment = (len(positives) * bonus_per_positive) - (len(negatives) * penalty_per_negative) | |
| # Cap adjustment so it doesn't dominate scoring | |
| adjustment_cap = 8.0 | |
| adjustment = clamp(raw_adjustment, -adjustment_cap, adjustment_cap) | |
| final_total = clamp(base_total + adjustment, 0.0, 100.0) | |
| return { | |
| "candidate_name": match_summary.get("candidate_name", ""), | |
| "total_score": round(final_total, 2), | |
| "breakdown": { | |
| "base_score": round(base_total, 2), | |
| "bonus_penalty_adjustment": round(adjustment, 2), | |
| "positive_count": len(positives), | |
| "negative_count": len(negatives), | |
| "must_have_coverage": round(must_cov, 3), | |
| "nice_to_have_coverage": round(nice_cov, 3), | |
| "experience_score": round(exp_sc, 3), | |
| "weights_normalized": weights | |
| } | |
| } | |