All demos
FintechLlamaIndex

Fraud Decision Explainer

Explain an already-blocked transaction (regulator-grade letter)

Ready to replay
0.0s / 9.9s

Security Pipeline

Input
Sandbox
Network
PII Scan
Injection
Vault
LLM Call
Result
Real run · LlamaIndex · python run.py · captured 2026-06-30 · SDK 1.3.0
Run it yourself
View the agent code· with Declawfintech-workflows/sandboxed/16-fraud-explainer-anthropic-nonstream-llamaindex/run.py
"""Fraud Decision Explainer — LlamaIndex + Anthropic Claude (sandboxed).

Same FunctionAgent as the baseline, wrapped in a Declaw sandbox under
`compliance_rag_policy`:
  * Outbound requests to api.anthropic.com have PAN/SSN/VPA/card-PAN redacted
    by the proxy before the request leaves the microVM, then rehydrated on the
    response — this is a genuine non-streaming `messages.create()` call (the
    proxy JSON-body redaction bug that used to 404 it, SDK #08, is fixed).
  * Injection defense scans with threshold=0.5 (data-egress-sensitive + judge,
    log_only) — attacker-supplied merchant descriptor text is detected and
    audited so it can't quietly hijack the narrative.
  * Allowlist = api.anthropic.com + api.openai.com + pypi bootstrap only;
    a compromised tool helper cannot exfil to a third domain.
"""
from __future__ import annotations

import json
import sys
import textwrap
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO_ROOT))
sys.path.insert(0, str(REPO_ROOT / "sandboxed"))

from shared.mock_customers import CUSTOMERS  # noqa: E402
from shared.mock_transactions import card_transactions, upi_transactions  # noqa: E402
from shared.mock_policies import CIRCULARS  # noqa: E402
from shared.declaw_helpers import (  # noqa: E402
    LLM_DOMAINS, compliance_rag_policy, llm_envs, run_python_in_sandbox,
)


# Payload assembled on the host so the sandbox only sees what it needs.
def _all_tx() -> list[dict]:
    txs = []
    for cid in CUSTOMERS:
        for t in card_transactions(cid):
            txs.append({**t, "customer_id": cid})
        for t in upi_transactions(cid):
            txs.append({**t, "customer_id": cid})
    return txs


AGENT_SCRIPT = textwrap.dedent("""
    import sys, json, anthropic as _ant
    print(f"[sandbox] anthropic SDK version: {_ant.__version__}", file=sys.stderr, flush=True)
    from llama_index.core.tools import FunctionTool
    from anthropic import Anthropic as AnthropicClient

    with open("/tmp/in.json") as f:
        inp = json.load(f)
    ALL_TX = inp["all_tx"]
    CUSTOMERS = inp["customers"]
    CIRCULARS = inp["circulars"]
    HINT = inp["hint"]
    CUSTOMER_ID = inp["customer_id"]

    def fetch_transaction(hint):
        \"\"\"Find a tx matching any identifier substring (rrn, pan_last4,
        merchant, vpa_to/from, description).\"\"\"
        needle = hint.lower()
        fields = ("rrn", "pan_last4", "merchant", "vpa_to", "vpa_from", "description")
        for t in ALL_TX:
            for key in fields:
                if needle in str(t.get(key, "")).lower():
                    return t
        return {"error": f"no match for {hint}"}

    def fetch_customer(customer_id: str) -> dict:
        return CUSTOMERS.get(customer_id, {"error": "no such customer"})

    def score_features(transaction: dict, customer: dict) -> dict:
        amt = transaction.get("amount_inr") or transaction.get("amount_usd") or 0
        return {
            "transaction_id": transaction.get("rrn") or transaction.get("merchant"),
            "velocity_last_hour": 4 if amt > 100000 else 1,
            "cross_border_flag": transaction.get("country") not in (None, "IN", "US"),
            "amount_z_score": 3.2 if amt > 100000 else 0.4,
            "known_good_merchant": transaction.get("merchant") in {"Swiggy", "Apple India"},
            "risk_flags": transaction.get("risk_flags") or [],
        }

    def lookup_policy(policy_id: str) -> dict:
        for c in CIRCULARS:
            if c["id"] == policy_id:
                return c
        return {"error": f"policy_id {policy_id} not found"}

    def draft_customer_letter(transaction: dict, customer: dict,
                              features: dict, policy_excerpt: str) -> str:
        \"\"\"Claude narrative generator — a native non-streaming
        messages.create() call through the Declaw proxy. (Earlier this had to
        use messages.stream() to dodge a proxy 404 on non-streaming Anthropic
        requests; that bug, SDK #08, is now fixed proxy-side.)\"\"\"
        client = AnthropicClient()
        resp = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=500,
            system=("You are a customer-facing fraud-operations specialist. "
                    "Produce a concise, defensible explanation letter (4-6 "
                    "sentences). Cite the relevant policy circular. Write at "
                    "7th-grade level. Close with appeal instructions. Any "
                    "[REDACTED_*] tokens are opaque placeholders."),
            messages=[{"role": "user", "content": json.dumps({
                "transaction": transaction, "customer": customer,
                "fraud_features": features, "policy_excerpt": policy_excerpt,
            })}],
        )
        return "".join(
            b.text for b in resp.content if getattr(b, "type", "") == "text")

    # Deterministic pipeline (same shape as the baseline). Each step is a
    # FunctionTool — kept for API-surface parity — but called directly
    # instead of through FunctionAgent's tool-calling loop.
    TOOLS = {
        "fetch_transaction":     FunctionTool.from_defaults(fn=fetch_transaction),
        "fetch_customer":        FunctionTool.from_defaults(fn=fetch_customer),
        "score_features":        FunctionTool.from_defaults(fn=score_features),
        "lookup_policy":         FunctionTool.from_defaults(fn=lookup_policy),
        "draft_customer_letter": FunctionTool.from_defaults(fn=draft_customer_letter),
    }

    def choose_policy_id(features, transaction):
        if transaction.get("cross_border_flag") or features.get("cross_border_flag"):
            return "FATF-REC-10"
        if features.get("amount_z_score", 0) >= 3.0:
            return "RBI-2025-DL-01"
        if str(transaction.get("rrn", "")).startswith("41"):
            return "RBI-2025-DL-01"
        return "PCI-DSS-3.2"

    tx = TOOLS["fetch_transaction"].fn(HINT)
    if "error" in tx:
        letter = f"(no transaction matched hint={HINT!r})"
    else:
        cust = TOOLS["fetch_customer"].fn(CUSTOMER_ID)
        features = TOOLS["score_features"].fn(tx, cust)
        pid = choose_policy_id(features, tx)
        policy = TOOLS["lookup_policy"].fn(pid)
        letter = TOOLS["draft_customer_letter"].fn(
            tx, cust, features, policy.get("excerpt", ""))
    with open("/tmp/out.json", "w") as f:
        json.dump({"letter": letter}, f)
""")


def main() -> None:
    customers_dict = {
        cid: {
            "id": c.id, "name": c.name,
            "pan": c.pan, "aadhaar": c.aadhaar, "ssn": c.ssn,
            "upi_vpa": c.upi_vpa, "email": c.email, "phone": c.phone,
            "cards_on_file": c.cards_on_file,
            "cibil_score": c.cibil_score, "fico_score": c.fico_score,
        }
        for cid, c in CUSTOMERS.items()
    }
    payload_common = {
        "all_tx": _all_tx(),
        "customers": customers_dict,
        "circulars": CIRCULARS,
    }

    demos = [("c-001", "UNKNOWN-MERCHANT-MOSCOW"), ("c-003", "acme.shellco")]
    for cid, hint in demos:
        print(f"\n=== Fraud Explainer (sandboxed, Anthropic non-stream) ===")
        print(f"Customer: {cid} — Hint: {hint!r}\n")
        print("[agent] entering compliance_rag_policy sandbox (Anthropic "
              "messages.create() non-stream — PII redacted outbound and "
              "rehydrated on the response; injected merchant-descriptor text "
              "is detected and audited)")
        pol = compliance_rag_policy(LLM_DOMAINS)
        out = run_python_in_sandbox(
            "fraud-explain", AGENT_SCRIPT, pol,
            payload={**payload_common, "customer_id": cid, "hint": hint},
            # anthropic + llama-index-core are both pre-baked in the
            # ai-agent template — no pip-install needed.
            envs=llm_envs(), timeout=240,
        )
        print("--- Letter ---")
        print(out.get("letter", "(no letter returned)"))


if __name__ == "__main__":
    main()
View raw audit JSON
[
  {
    "atMs": 450,
    "kind": "stage",
    "payload": {
      "stage": "input",
      "status": "done",
      "detail": "read input files in-VM"
    }
  },
  {
    "atMs": 1400,
    "kind": "stage",
    "payload": {
      "stage": "sandbox",
      "status": "done",
      "detail": "4 Firecracker microVM(s) · own kernel · egress-locked"
    }
  },
  {
    "atMs": 2350,
    "kind": "network",
    "payload": {
      "event": "egress_allowed",
      "detail": {
        "host": "api.anthropic.com",
        "port": 443,
        "reason": "allowlist"
      }
    }
  },
  {
    "atMs": 3300,
    "kind": "security",
    "payload": {
      "event": "pii_redaction",
      "detail": {
        "entities": [
          {
            "entity_type": "EMAIL_ADDRESS",
            "masked_value": "REDACTED_EMAIL_ADDRESS_43",
            "confidence": 1
          },
          {
            "entity_type": "CREDIT_CARD",
            "masked_value": "REDACTED_CREDIT_CARD_38",
            "confidence": 1
          },
          {
            "entity_type": "PERSON",
            "masked_value": "REDACTED_PERSON_2",
            "confidence": 0.85
          },
          {
            "entity_type": "PERSON",
            "masked_value": "REDACTED_PERSON_3",
            "confidence": 0.85
          },
          {
            "entity_type": "PHONE_NUMBER",
            "masked_value": "REDACTED_PHONE_NUMBER_37",
            "confidence": 0.75
          },
          {
            "entity_type": "US_SSN",
            "masked_value": "REDACTED_US_SSN_38",
            "confidence": 0.5
          }
        ],
        "destination": "api.anthropic.com",
        "rehydrated_on_reply": true
      }
    }
  },
  {
    "atMs": 4250,
    "kind": "audit",
    "payload": {
      "event": "pii_redaction",
      "category": "security",
      "detail": {
        "entities": 6,
        "destination": "api.anthropic.com",
        "rehydrated": true
      },
      "timestamp": "2026-06-30T17:54:47.656289Z"
    }
  },
  {
    "atMs": 5200,
    "kind": "security",
    "payload": {
      "event": "pii_redaction",
      "detail": {
        "entities": [
          {
            "entity_type": "EMAIL_ADDRESS",
            "masked_value": "REDACTED_EMAIL_ADDRESS_43",
            "confidence": 1
          },
          {
            "entity_type": "CREDIT_CARD",
            "masked_value": "REDACTED_CREDIT_CARD_38",
            "confidence": 1
          },
          {
            "entity_type": "PERSON",
            "masked_value": "REDACTED_PERSON_2",
            "confidence": 0.85
          },
          {
            "entity_type": "PERSON",
            "masked_value": "REDACTED_PERSON_3",
            "confidence": 0.85
          },
          {
            "entity_type": "PHONE_NUMBER",
            "masked_value": "REDACTED_PHONE_NUMBER_37",
            "confidence": 0.75
          },
          {
            "entity_type": "US_SSN",
            "masked_value": "REDACTED_US_SSN_38",
            "confidence": 0.5
          }
        ],
        "destination": "api.anthropic.com",
        "rehydrated_on_reply": true
      }
    }
  },
  {
    "atMs": 6150,
    "kind": "audit",
    "payload": {
      "event": "pii_redaction",
      "category": "security",
      "detail": {
        "entities": 6,
        "destination": "api.anthropic.com",
        "rehydrated": true
      },
      "timestamp": "2026-06-30T18:33:07.655494Z"
    }
  },
  {
    "atMs": 7100,
    "kind": "security",
    "payload": {
      "event": "vault_brokered",
      "detail": {
        "keys": "ANTHROPIC_API_KEY",
        "host": "api.anthropic.com",
        "injected_at": "egress proxy",
        "exposure_to_vm": "none (declaw:vault-managed placeholder)"
      }
    }
  },
  {
    "atMs": 8050,
    "kind": "stage",
    "payload": {
      "stage": "llm",
      "status": "done",
      "detail": "model called from inside the microVM (PII redacted on the wire) · 2345.0s real",
      "durationMs": 1700
    }
  },
  {
    "atMs": 9000,
    "kind": "decision",
    "payload": {
      "text": "Decline / appeal letter drafted — explains an upstream decision, makes none"
    }
  }
]