Spaces:
Sleeping
Sleeping
| """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" | |