Niketjain2002 commited on
Commit
b7e194c
·
verified ·
1 Parent(s): a483a8f

Upload src/explainer.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/explainer.py +102 -0
src/explainer.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Explanation Layer
3
+
4
+ Produces human-readable explanations of the scoring results.
5
+ v1: LLM-generated explanations with template structure
6
+ v2: Will use SHAP/LIME feature importance from ML model
7
+ """
8
+
9
+ import json
10
+ from typing import Optional
11
+
12
+ from .feature_extractor import LLMClient, _extract_json
13
+ from .prompts.scoring import EXPLANATION_PROMPT
14
+
15
+
16
+ class Explainer:
17
+ """Generates human-readable explanations for probability scores."""
18
+
19
+ def __init__(self, llm_client: Optional[LLMClient] = None):
20
+ self.llm = llm_client or LLMClient()
21
+
22
+ def explain(self, scoring_results: dict, match_analysis: dict) -> dict:
23
+ """Generate explanation from scoring results and match data."""
24
+ prompt = EXPLANATION_PROMPT.format(
25
+ scoring_results=json.dumps(scoring_results, indent=2),
26
+ match_analysis=json.dumps(match_analysis, indent=2),
27
+ )
28
+ response = self.llm.complete(prompt, temperature=0.2)
29
+ return _extract_json(response)
30
+
31
+ def explain_deterministic(self, calibrated: dict, match_analysis: dict) -> dict:
32
+ """
33
+ Rule-based explanation generation. No LLM needed.
34
+ Useful for testing and as a structured fallback.
35
+ """
36
+ positive = []
37
+ risks = []
38
+ missing = []
39
+
40
+ # Build positive signals
41
+ skill_match = match_analysis.get("skill_match_analysis", {})
42
+ coverage = skill_match.get("coverage_ratio", 0)
43
+ if coverage >= 0.8:
44
+ matched = len(skill_match.get("matched_must_haves", []))
45
+ positive.append(f"Strong skill coverage: {matched} of required skills matched ({coverage:.0%})")
46
+ elif coverage >= 0.5:
47
+ positive.append(f"Moderate skill coverage at {coverage:.0%} of required skills")
48
+
49
+ for signal in match_analysis.get("positive_signals", []):
50
+ positive.append(f"{signal['signal']} ({signal.get('strength', 'moderate')}): {signal.get('evidence', '')}")
51
+
52
+ # Build risk signals
53
+ for risk in match_analysis.get("risk_flags", []):
54
+ risks.append(f"{risk['risk']} [{risk.get('severity', 'medium')}]: {risk.get('evidence', '')}")
55
+
56
+ # Missing signals
57
+ for item in match_analysis.get("missing_information", []):
58
+ missing.append(item)
59
+
60
+ # Build summary
61
+ overall = calibrated.get("overall_hire_probability", 0)
62
+ shortlist = calibrated.get("shortlist_probability", 0)
63
+
64
+ if overall >= 60:
65
+ tone = "Strong candidate"
66
+ elif overall >= 40:
67
+ tone = "Moderate candidate"
68
+ elif overall >= 20:
69
+ tone = "Below-average fit"
70
+ else:
71
+ tone = "Poor fit"
72
+
73
+ summary = (
74
+ f"{tone} with {overall}% estimated hire probability. "
75
+ f"Shortlist probability is {shortlist}%. "
76
+ )
77
+
78
+ if risks:
79
+ summary += f"Key risk: {risks[0].split(':')[0]}. "
80
+ if positive:
81
+ summary += f"Key strength: {positive[0].split(':')[0]}."
82
+
83
+ # Recommendation
84
+ if overall >= 60:
85
+ rec = "pass"
86
+ elif overall >= 45:
87
+ rec = "borderline"
88
+ elif overall >= 25:
89
+ rec = "no_pass"
90
+ else:
91
+ rec = "strong_no_pass"
92
+
93
+ if overall >= 75:
94
+ rec = "strong_pass"
95
+
96
+ return {
97
+ "reasoning_summary": summary,
98
+ "positive_signals": positive[:6],
99
+ "risk_signals": risks[:6],
100
+ "missing_signals": missing[:4],
101
+ "recommendation": rec,
102
+ }