Kreb1907's picture
Upload folder using huggingface_hub
176e552 verified
Raw
History Blame Contribute Delete
4.12 kB
"""Natural-language explanation of a scored transaction.
Tries an LLM through Hugging Face Inference Providers when an ``HF_TOKEN``
secret is configured on the Space; otherwise falls back to a deterministic
rule-based narrative built from the same SHAP values, so the app always works.
"""
import os
from model import FEATURE_FMT, FEATURE_LABELS
LLM_MODEL = os.environ.get("LLM_MODEL", "meta-llama/Llama-3.1-8B-Instruct")
def risk_band(p: float) -> tuple[str, str]:
"""Return (band name, recommended action)."""
if p < 0.05:
return "LOW", "Approve normally."
if p < 0.30:
return "ELEVATED", "Approve, but flag the account for passive monitoring."
if p < 0.70:
return "HIGH", "Trigger step-up authentication (3-D Secure / OTP) before approving."
return "CRITICAL", "Decline and route to manual fraud review immediately."
def _ranked(shap_values: dict) -> list[tuple[str, float]]:
return sorted(shap_values.items(), key=lambda kv: abs(kv[1]), reverse=True)
def _describe(feature: str, inputs: dict) -> str:
return f"{FEATURE_LABELS[feature].lower()} = {FEATURE_FMT[feature](inputs[feature])}"
def template_explanation(result: dict) -> str:
"""Deterministic analyst-style narrative from the SHAP ranking."""
p = result["probability"]
band, action = risk_band(p)
ranked = _ranked(result["shap_values"])
inputs = result["inputs"]
drivers = [(f, v) for f, v in ranked if v > 0.05][:3]
mitigators = [(f, v) for f, v in ranked if v < -0.05][:2]
lines = [
f"**Verdict:** this transaction scores **{p:.1%}** fraud probability — **{band}** risk."
]
if drivers:
parts = [_describe(f, inputs) for f, _ in drivers]
lines.append(
"The score is driven mainly by "
+ (", ".join(parts[:-1]) + " and " + parts[-1] if len(parts) > 1 else parts[0])
+ "."
)
if mitigators:
parts = [_describe(f, inputs) for f, _ in mitigators]
lines.append(
"Working in the customer's favour: "
+ (" and ".join(parts))
+ "."
)
lines.append(f"**Recommended action:** {action}")
return "\n\n".join(lines)
def llm_explanation(result: dict) -> tuple[str, str]:
"""Return (markdown_text, source) where source is 'llm' or 'rules'."""
token = os.environ.get("HF_TOKEN")
if not token:
return template_explanation(result), "rules"
try:
from huggingface_hub import InferenceClient
p = result["probability"]
band, action = risk_band(p)
ranked = _ranked(result["shap_values"])
inputs = result["inputs"]
shap_lines = "\n".join(
f"- {FEATURE_LABELS[f]} = {FEATURE_FMT[f](inputs[f])}: SHAP {v:+.3f}"
for f, v in ranked
)
prompt = (
f"A gradient-boosted fraud model scored a card transaction at "
f"{p:.1%} fraud probability ({band} risk; policy action: {action}).\n"
f"SHAP contributions in log-odds (positive pushes toward fraud):\n"
f"{shap_lines}\n\n"
"Write a fraud analyst's explanation in 3–5 sentences: state the "
"verdict, explain the top risk drivers in plain language, mention "
"any mitigating factors, and end with the recommended action. "
"Do not mention SHAP or log-odds; talk about the transaction itself."
)
client = InferenceClient(api_key=token)
out = client.chat_completion(
model=LLM_MODEL,
messages=[
{
"role": "system",
"content": "You are a senior fraud analyst writing concise, factual case notes.",
},
{"role": "user", "content": prompt},
],
max_tokens=350,
temperature=0.3,
)
text = out.choices[0].message.content.strip()
if not text:
raise ValueError("empty completion")
return text, "llm"
except Exception:
return template_explanation(result), "rules"