Niketjain2002's picture
Upload src/explainer.py with huggingface_hub
b7e194c verified
"""
Explanation Layer
Produces human-readable explanations of the scoring results.
v1: LLM-generated explanations with template structure
v2: Will use SHAP/LIME feature importance from ML model
"""
import json
from typing import Optional
from .feature_extractor import LLMClient, _extract_json
from .prompts.scoring import EXPLANATION_PROMPT
class Explainer:
"""Generates human-readable explanations for probability scores."""
def __init__(self, llm_client: Optional[LLMClient] = None):
self.llm = llm_client or LLMClient()
def explain(self, scoring_results: dict, match_analysis: dict) -> dict:
"""Generate explanation from scoring results and match data."""
prompt = EXPLANATION_PROMPT.format(
scoring_results=json.dumps(scoring_results, indent=2),
match_analysis=json.dumps(match_analysis, indent=2),
)
response = self.llm.complete(prompt, temperature=0.2)
return _extract_json(response)
def explain_deterministic(self, calibrated: dict, match_analysis: dict) -> dict:
"""
Rule-based explanation generation. No LLM needed.
Useful for testing and as a structured fallback.
"""
positive = []
risks = []
missing = []
# Build positive signals
skill_match = match_analysis.get("skill_match_analysis", {})
coverage = skill_match.get("coverage_ratio", 0)
if coverage >= 0.8:
matched = len(skill_match.get("matched_must_haves", []))
positive.append(f"Strong skill coverage: {matched} of required skills matched ({coverage:.0%})")
elif coverage >= 0.5:
positive.append(f"Moderate skill coverage at {coverage:.0%} of required skills")
for signal in match_analysis.get("positive_signals", []):
positive.append(f"{signal['signal']} ({signal.get('strength', 'moderate')}): {signal.get('evidence', '')}")
# Build risk signals
for risk in match_analysis.get("risk_flags", []):
risks.append(f"{risk['risk']} [{risk.get('severity', 'medium')}]: {risk.get('evidence', '')}")
# Missing signals
for item in match_analysis.get("missing_information", []):
missing.append(item)
# Build summary
overall = calibrated.get("overall_hire_probability", 0)
shortlist = calibrated.get("shortlist_probability", 0)
if overall >= 60:
tone = "Strong candidate"
elif overall >= 40:
tone = "Moderate candidate"
elif overall >= 20:
tone = "Below-average fit"
else:
tone = "Poor fit"
summary = (
f"{tone} with {overall}% estimated hire probability. "
f"Shortlist probability is {shortlist}%. "
)
if risks:
summary += f"Key risk: {risks[0].split(':')[0]}. "
if positive:
summary += f"Key strength: {positive[0].split(':')[0]}."
# Recommendation
if overall >= 60:
rec = "pass"
elif overall >= 45:
rec = "borderline"
elif overall >= 25:
rec = "no_pass"
else:
rec = "strong_no_pass"
if overall >= 75:
rec = "strong_pass"
return {
"reasoning_summary": summary,
"positive_signals": positive[:6],
"risk_signals": risks[:6],
"missing_signals": missing[:4],
"recommendation": rec,
}