# %%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 } }