All demos
FintechLlamaIndex
Fraud Decision Explainer
Explain an already-blocked transaction (regulator-grade letter)
Ready to replay
0.0s / 9.9sSecurity 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"
}
}
]