ledgershield / inference_improved.py
king673134's picture
Upload folder using huggingface_hub
1ed9b86 verified
Raw
History Blame Contribute Delete
46.5 kB
"""
Improved LedgerShield inference with better LLM prompting and validation.
Fixes issues with SOTA models returning empty evidence/reasons.
"""
from __future__ import annotations
import os
import sys
if os.getenv("LEDGERSHIELD_DEBUG") != "1":
sys.stderr = open(os.devnull, "w", encoding="utf-8")
import argparse
import json
import re
import warnings
from typing import Any, Optional
warnings.filterwarnings("ignore", category=DeprecationWarning)
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
from openai import OpenAI
from ledgershield_env import LedgerShieldAction, LedgerShieldEnv
API_BASE_URL = os.getenv("API_BASE_URL") or "https://api.openai.com/v1"
MODEL_NAME = os.getenv("MODEL_NAME") or "gpt-4o-mini"
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
API_KEY = OPENAI_API_KEY or os.getenv("API_KEY")
ENV_URL = os.getenv("ENV_URL") or "http://localhost:8000"
BENCHMARK = "ledgershield"
MAX_STEPS = 20
TEMPERATURE = 0.0
MAX_TOKENS = 1024 # Increased for better reasoning
SUCCESS_SCORE_THRESHOLD = 0.85
DEFAULT_CASES = [
"CASE-A-001",
"CASE-A-002",
"CASE-A-003",
"CASE-A-004",
"CASE-B-001",
"CASE-B-002",
"CASE-B-003",
"CASE-B-004",
"CASE-B-005",
"CASE-C-001",
"CASE-C-002",
"CASE-C-003",
"CASE-C-004",
"CASE-D-001",
"CASE-D-002",
"CASE-D-003",
"CASE-D-004",
"CASE-D-005",
"CASE-D-006",
"CASE-E-001",
"CASE-E-002",
]
API_CALLS_TOTAL = 0
API_TOKENS_PROMPT = 0
API_TOKENS_COMPLETION = 0
API_TOKENS_TOTAL = 0
def reset_api_tracking():
global API_CALLS_TOTAL, API_TOKENS_PROMPT, API_TOKENS_COMPLETION, API_TOKENS_TOTAL
API_CALLS_TOTAL = 0
API_TOKENS_PROMPT = 0
API_TOKENS_COMPLETION = 0
API_TOKENS_TOTAL = 0
def track_api_usage(usage):
global API_CALLS_TOTAL, API_TOKENS_PROMPT, API_TOKENS_COMPLETION, API_TOKENS_TOTAL
if usage:
API_CALLS_TOTAL += 1
API_TOKENS_PROMPT += usage.prompt_tokens or 0
API_TOKENS_COMPLETION += usage.completion_tokens or 0
API_TOKENS_TOTAL += usage.total_tokens or 0
def print_api_summary():
cost_estimate = API_TOKENS_TOTAL * 0.000005
print(f"\n{'='*60}")
print(f"API USAGE SUMMARY")
print(f"{'='*60}")
print(f"Model: {MODEL_NAME}")
print(f"Total API calls: {API_CALLS_TOTAL}")
print(f"Prompt tokens: {API_TOKENS_PROMPT:,}")
print(f"Completion tokens: {API_TOKENS_COMPLETION:,}")
print(f"Total tokens: {API_TOKENS_TOTAL:,}")
print(f"Estimated cost: ${cost_estimate:.4f}")
print(f"{'='*60}\n")
def normalize_text(value: Any) -> str:
if value is None:
return ""
return " ".join(str(value).strip().lower().split())
def safe_float(value: Any) -> float:
try:
if isinstance(value, str):
cleaned = value.replace(",", "").replace("$", "").replace("€", "").replace("₹", "").strip()
return float(cleaned)
return float(value)
except Exception:
return 0.0
def clamp(value: float, lo: float, hi: float) -> float:
return max(lo, min(hi, value))
def compact_json(value: Any) -> str:
return json.dumps(value, separators=(",", ":"), ensure_ascii=True, sort_keys=True)
def sanitize_log_field(value: Any) -> str:
if value is None:
return "null"
text = " ".join(str(value).split())
return text if text else "null"
def format_action(action: LedgerShieldAction) -> str:
return f"{action.action_type}({compact_json(action.payload)})"
def log_start(task: str, env: str, model: str) -> None:
print(f"[START] task={task} env={env} model={sanitize_log_field(model)}", flush=True)
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
print(
"[STEP] "
f"step={step} "
f"action={sanitize_log_field(action)} "
f"reward={reward:.2f} "
f"done={str(done).lower()} "
f"error={sanitize_log_field(error)}",
flush=True,
)
def log_end(success: bool, steps: int, rewards: list[float]) -> None:
rewards_str = ",".join(f"{reward:.2f}" for reward in rewards)
print(
"[END] "
f"success={str(success).lower()} "
f"steps={steps} "
f"rewards={rewards_str}",
flush=True,
)
def trace(message: str) -> None:
if os.getenv("LEDGERSHIELD_DEBUG") == "1":
print(message, file=sys.stderr, flush=True)
def token_ref(token: dict[str, Any], doc_id: str) -> dict[str, Any]:
return {
"doc_id": doc_id,
"page": int(token.get("page", 1) or 1),
"bbox": token.get("bbox", []),
"token_ids": [str(token.get("token_id", ""))],
}
def parse_invoice_tokens(tokens: list[dict[str, Any]], doc_id: str) -> tuple[dict[str, Any], dict[str, Any], list[dict[str, Any]]]:
fields: dict[str, Any] = {}
evidence: dict[str, Any] = {}
line_items: list[dict[str, Any]] = []
for idx, token in enumerate(tokens):
text = str(token.get("text", "")).strip()
lower = text.lower()
if idx == 0 and text:
fields["vendor_name"] = text
evidence["vendor_name"] = token_ref(token, doc_id)
continue
matchers = [
("invoice_number", r"invoice\s*no\s*:\s*(.+)$"),
("invoice_date", r"invoice\s*date\s*:\s*(.+)$"),
("currency", r"currency\s*:\s*(.+)$"),
("po_id", r"po\s*:\s*(.+)$"),
("receipt_id", r"receipt\s*:\s*(.+)$"),
("bank_account", r"bank\s*:\s*(.+)$"),
]
matched_field = False
for key, pattern in matchers:
match = re.match(pattern, text, flags=re.IGNORECASE)
if match:
fields[key] = match.group(1).strip()
evidence[key] = token_ref(token, doc_id)
matched_field = True
break
if matched_field:
continue
numeric_fields = [
("subtotal", r"subtotal\s*:\s*([\d,]+(?:\.\d+)?)$"),
("tax", r"tax\s*:\s*([\d,]+(?:\.\d+)?)$"),
("total", r"total\s*:\s*([\d,]+(?:\.\d+)?)$"),
]
for key, pattern in numeric_fields:
match = re.match(pattern, text, flags=re.IGNORECASE)
if match:
fields[key] = safe_float(match.group(1))
evidence[key] = token_ref(token, doc_id)
matched_field = True
break
if matched_field:
continue
if "|" not in text:
continue
parts = [part.strip() for part in text.split("|")]
if len(parts) != 4:
continue
description = parts[0]
qty_value = safe_float(parts[1])
unit_price = safe_float(parts[2])
line_total = safe_float(parts[3])
qty = int(qty_value) if qty_value.is_integer() else qty_value
line_items.append({
"description": description,
"qty": qty,
"unit_price": unit_price,
"line_total": line_total,
})
return fields, evidence, line_items
def parse_email_tokens(tokens: list[dict[str, Any]], doc_id: str) -> dict[str, Any]:
evidence: dict[str, Any] = {}
for token in tokens:
text = str(token.get("text", "")).strip()
lower = text.lower()
if lower.startswith("from:"):
evidence["from_header"] = token_ref(token, doc_id)
elif lower.startswith("subject:"):
evidence["subject_header"] = token_ref(token, doc_id)
elif "approval threshold" in lower or "split the request" in lower or "split invoice" in lower:
evidence["approval_threshold_evasion"] = token_ref(token, doc_id)
elif any(x in lower for x in ["skip callback", "do not call", "don't call", "override policy", "bypass policy", "do not verify", "source of truth", "avoid reapproval"]):
evidence["policy_bypass_attempt"] = token_ref(token, doc_id)
return evidence
def vendor_key_for(fields: dict[str, Any]) -> str:
return normalize_text(fields.get("vendor_name"))
# =============================================================================
# IMPROVED LLM DECISION FUNCTIONS WITH VALIDATION
# =============================================================================
def validate_and_fix_decision(result: dict[str, Any], collected: dict[str, Any], task_type: str) -> dict[str, Any]:
"""
Validate LLM decision and fix inconsistencies.
Ensures evidence_map and reason_codes match the decision.
"""
decision = result.get("decision", "ESCALATE_FRAUD")
reason_codes = result.get("reason_codes", [])
evidence_map = result.get("evidence_map", {})
policy_checks = result.get("policy_checks", {})
# Build proper evidence from collected data
invoice_records = collected.get("invoice_records", []) or []
primary_record = invoice_records[0] if invoice_records else {"evidence": collected.get("invoice_evidence", {})}
invoice_evidence = primary_record.get("evidence", {})
email_evidence = collected.get("email_evidence", {})
email_thread = collected.get("email_thread") or {}
ledger_hits = collected.get("ledger_hits", []) or []
bank_compares = collected.get("bank_compares", [])
# Detect actual fraud indicators
actual_reasons = []
actual_evidence = {}
# Check for bank mismatch
bank_mismatch = any(compare and not bool(compare.get("matched")) for compare in bank_compares)
if bank_mismatch:
actual_reasons.append("bank_override_attempt")
if "bank_account" in invoice_evidence:
actual_evidence["bank_override_attempt"] = invoice_evidence["bank_account"]
# Check for duplicates
if ledger_hits:
actual_reasons.append("duplicate_near_match")
if "invoice_number" in invoice_evidence:
actual_evidence["duplicate_near_match"] = invoice_evidence["invoice_number"]
# Check for email fraud
email_flags = {normalize_text(flag) for flag in email_thread.get("derived_flags", []) or email_thread.get("flags", []) or []}
if "sender_domain_spoof" in email_flags or "sender_domain_spoof" in email_evidence:
actual_reasons.append("sender_domain_spoof")
if "from_header" in email_evidence:
actual_evidence["sender_domain_spoof"] = email_evidence["from_header"]
if "policy_bypass_attempt" in email_flags or "policy_bypass_attempt" in email_evidence:
actual_reasons.append("policy_bypass_attempt")
if email_evidence.get("policy_bypass_attempt"):
actual_evidence["policy_bypass_attempt"] = email_evidence["policy_bypass_attempt"]
if "approval_threshold_evasion" in email_flags or email_evidence.get("approval_threshold_evasion"):
actual_reasons.append("approval_threshold_evasion")
if email_evidence.get("approval_threshold_evasion"):
actual_evidence["approval_threshold_evasion"] = email_evidence["approval_threshold_evasion"]
# Determine correct decision based on actual evidence
has_fraud = len(actual_reasons) > 0
correct_decision = "ESCALATE_FRAUD" if has_fraud else "PAY"
# If LLM decision conflicts with evidence, use evidence-based decision
if decision == "ESCALATE_FRAUD" and not has_fraud:
# LLM says fraud but no evidence - downgrade to PAY
trace(f"[VALIDATION] LLM said ESCALATE but no fraud evidence found. Correcting to PAY.")
decision = "PAY"
reason_codes = []
evidence_map = {}
elif decision == "PAY" and has_fraud:
# LLM says PAY but fraud exists - upgrade to ESCALATE
trace(f"[VALIDATION] LLM said PAY but fraud evidence found. Correcting to ESCALATE_FRAUD.")
decision = "ESCALATE_FRAUD"
reason_codes = actual_reasons
evidence_map = actual_evidence
elif decision == "ESCALATE_FRAUD" and has_fraud:
# LLM is correct - ensure evidence is complete
if not reason_codes:
reason_codes = actual_reasons
if not evidence_map:
evidence_map = actual_evidence
# Ensure policy_checks match reason_codes
if not policy_checks or not isinstance(policy_checks, dict):
policy_checks = {
"three_way_match": "pass",
"bank_change_verification": "fail" if "bank_override_attempt" in reason_codes else "pass",
"duplicate_check": "fail" if "duplicate_near_match" in reason_codes else "pass",
"approval_threshold_check": "fail" if "approval_threshold_evasion" in reason_codes else "pass",
}
# Generate counterfactual if missing
counterfactual = result.get("counterfactual", "")
if not counterfactual:
if decision == "ESCALATE_FRAUD":
counterfactual = "Would PAY if " + ", ".join([
"bank account matched vendor master",
"no duplicate invoices found",
"email domain verified",
"no policy bypass attempts"
][:min(2, len(reason_codes) if reason_codes else 1)]) + "."
else:
counterfactual = "Would HOLD if discrepancies found or fraud indicators present."
return {
"decision": decision,
"confidence": clamp(float(result.get("confidence", 0.9)), 0.0, 1.0),
"reason_codes": reason_codes,
"policy_checks": policy_checks,
"evidence_map": evidence_map,
"counterfactual": counterfactual,
}
def llm_decision_task_b(client: Optional[OpenAI], collected: dict[str, Any]) -> dict[str, Any]:
"""Use LLM to analyze evidence and decide PAY or HOLD for Task B."""
if not client:
return heuristic_task_b(collected)
invoice_fields = collected["invoice_fields"]
po = collected.get("po") or {}
receipt = collected.get("receipt")
context = {
"task": "Task B - Three-way match decisioning",
"invoice_fields": invoice_fields,
"po_data": po,
"receipt_data": receipt,
"invoice_lines": collected.get("invoice_line_items", []),
}
system_prompt = """You are an expert AP (Accounts Payable) auditor. Analyze the invoice data and determine if it should be PAID or HELD.
Available evidence:
- Invoice fields extracted from document
- Purchase Order (PO) data
- Goods Receipt Note (GRN) / Receipt data
Decision rules:
- PAY: Invoice matches PO and receipt (valid three-way match)
- HOLD: Discrepancies found (price mismatch, missing receipt, quantity mismatch, total mismatch)
IMPORTANT: You must identify specific discrepancies and provide evidence.
Return JSON format:
{
"decision": "PAY" or "HOLD",
"confidence": float (0.0-1.0),
"discrepancies": [list of discrepancy types],
"reasoning": "brief explanation of your analysis"
}"""
try:
response = client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": compact_json(context)},
],
temperature=TEMPERATURE,
max_completion_tokens=MAX_TOKENS,
)
content = response.choices[0].message.content or "{}"
track_api_usage(response.usage)
result = json.loads(content)
decision = result.get("decision", "HOLD")
if decision not in ["PAY", "HOLD"]:
decision = "HOLD"
discrepancies = result.get("discrepancies", [])
if not isinstance(discrepancies, list):
discrepancies = []
# Build evidence map based on discrepancies
evidence_map = {}
invoice_evidence = collected["invoice_evidence"]
if "missing_receipt" in discrepancies and "po_id" in invoice_evidence:
evidence_map["missing_receipt"] = invoice_evidence["po_id"]
if "price_mismatch" in discrepancies and collected.get("invoice_line_tokens"):
evidence_map["price_mismatch"] = token_ref(collected["invoice_line_tokens"][0], collected["invoice_doc_id"])
if "total_mismatch" in discrepancies and "total" in invoice_evidence:
evidence_map["total_mismatch"] = invoice_evidence["total"]
return {
"decision": decision,
"confidence": clamp(float(result.get("confidence", 0.9)), 0.0, 1.0),
"discrepancies": discrepancies,
"policy_checks": {
"three_way_match": "fail" if discrepancies else "pass",
"bank_change_verification": "pass",
"duplicate_check": "pass",
"approval_threshold_check": "pass",
},
"evidence_map": evidence_map,
}
except Exception as e:
trace(f"[LLM ERROR] Task B: {e}")
return heuristic_task_b(collected)
def heuristic_task_b(collected: dict[str, Any]) -> dict[str, Any]:
"""Original deterministic logic as fallback."""
invoice_fields = collected["invoice_fields"]
invoice_evidence = collected["invoice_evidence"]
invoice_lines = collected["invoice_line_items"]
po = collected.get("po") or {}
receipt = collected.get("receipt")
discrepancies: list[str] = []
evidence_map: dict[str, Any] = {}
if receipt is None:
discrepancies.append("missing_receipt")
if "po_id" in invoice_evidence:
evidence_map["missing_receipt"] = invoice_evidence["po_id"]
else:
po_lines = po.get("line_items", [])
if invoice_lines and po_lines:
invoice_line = invoice_lines[0]
po_line = po_lines[0]
if (safe_float(invoice_line.get("unit_price")) != safe_float(po_line.get("unit_price")) or
safe_float(invoice_line.get("line_total")) != safe_float(po_line.get("line_total"))):
discrepancies.append("price_mismatch")
if invoice_lines:
evidence_map["price_mismatch"] = token_ref(collected["invoice_line_tokens"][0], collected["invoice_doc_id"])
if safe_float(invoice_fields.get("total")) != safe_float(po.get("total")):
discrepancies.append("total_mismatch")
if "total" in invoice_evidence:
evidence_map["total_mismatch"] = invoice_evidence["total"]
return {
"decision": "HOLD" if discrepancies else "PAY",
"confidence": 0.93 if discrepancies else 0.89,
"discrepancies": discrepancies,
"policy_checks": {
"three_way_match": "fail" if discrepancies else "pass",
"bank_change_verification": "pass",
"duplicate_check": "pass",
"approval_threshold_check": "pass",
},
"evidence_map": evidence_map,
}
def llm_decision_task_c(client: Optional[OpenAI], collected: dict[str, Any]) -> dict[str, Any]:
"""Use LLM to detect fraud and decide PAY or ESCALATE_FRAUD for Task C."""
if not client:
return heuristic_task_c(collected)
invoice_evidence = collected["invoice_evidence"]
ledger_search = collected.get("ledger_search") or {}
duplicate_links = [hit.get("ledger_id") for hit in collected.get("ledger_hits", []) if hit.get("ledger_id")]
bank_compare = collected.get("bank_compare") or {}
context = {
"task": "Task C - Duplicate and fraud triage",
"invoice_fields": collected.get("invoice_fields", {}),
"bank_comparison": bank_compare,
"ledger_search_results": ledger_search,
"duplicate_links": duplicate_links,
"vendor_history": collected.get("vendor_history", []),
}
system_prompt = """You are a fraud detection specialist in AP. Analyze the invoice for fraud indicators.
Fraud signals to watch for:
- Bank account mismatch (proposed account != vendor master)
- Duplicate invoices (same invoice number or amount in ledger)
- Suspicious vendor history
Decision:
- PAY: Clean, no fraud signals detected
- ESCALATE_FRAUD: Fraud indicators present (bank mismatch, duplicates, suspicious patterns)
IMPORTANT: If escalating, you MUST specify which fraud indicators were found.
Return JSON format:
{
"decision": "PAY" or "ESCALATE_FRAUD",
"confidence": float (0.0-1.0),
"fraud_flags": ["bank_override_attempt", "duplicate_near_match"],
"reasoning": "explanation of fraud indicators found"
}"""
try:
response = client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": compact_json(context)},
],
temperature=TEMPERATURE,
max_completion_tokens=MAX_TOKENS,
)
content = response.choices[0].message.content or "{}"
track_api_usage(response.usage)
result = json.loads(content)
decision = result.get("decision", "ESCALATE_FRAUD")
if decision not in ["PAY", "ESCALATE_FRAUD"]:
decision = "ESCALATE_FRAUD"
fraud_flags = result.get("fraud_flags", [])
if not isinstance(fraud_flags, list):
fraud_flags = []
# Build evidence map
evidence_map = {}
if "bank_override_attempt" in fraud_flags and "bank_account" in invoice_evidence:
evidence_map["bank_override_attempt"] = invoice_evidence["bank_account"]
if "duplicate_near_match" in fraud_flags and "invoice_number" in invoice_evidence:
evidence_map["duplicate_near_match"] = invoice_evidence["invoice_number"]
return {
"decision": decision,
"confidence": clamp(float(result.get("confidence", 0.9)), 0.0, 1.0),
"duplicate_links": duplicate_links if decision == "ESCALATE_FRAUD" else [],
"fraud_flags": fraud_flags,
"evidence_map": evidence_map,
}
except Exception as e:
trace(f"[LLM ERROR] Task C: {e}")
return heuristic_task_c(collected)
def heuristic_task_c(collected: dict[str, Any]) -> dict[str, Any]:
"""Original deterministic logic as fallback."""
invoice_evidence = collected["invoice_evidence"]
ledger_search = collected.get("ledger_search") or {}
duplicate_links = [hit.get("ledger_id") for hit in collected.get("ledger_hits", []) if hit.get("ledger_id")]
bank_compare = collected.get("bank_compare") or {}
bank_mismatch = bool(bank_compare) and not bool(bank_compare.get("matched"))
duplicate_detected = bool(duplicate_links) or int(ledger_search.get("exact_duplicate_count", 0) or 0) > 0
suspicious = bank_mismatch or duplicate_detected
evidence_map = {}
fraud_flags = []
if bank_mismatch and "bank_account" in invoice_evidence:
evidence_map["bank_override_attempt"] = invoice_evidence["bank_account"]
fraud_flags.append("bank_override_attempt")
if duplicate_detected and "invoice_number" in invoice_evidence:
evidence_map["duplicate_near_match"] = invoice_evidence["invoice_number"]
fraud_flags.append("duplicate_near_match")
return {
"decision": "ESCALATE_FRAUD" if suspicious else "PAY",
"confidence": 0.98 if suspicious else 0.87,
"duplicate_links": duplicate_links if suspicious else [],
"fraud_flags": fraud_flags,
"evidence_map": evidence_map,
}
def llm_decision_task_d(client: Optional[OpenAI], collected: dict[str, Any]) -> dict[str, Any]:
"""Use LLM to analyze complex fraud patterns for Task D with validation."""
if not client:
return heuristic_task_d(collected)
invoice_records = collected.get("invoice_records", []) or []
email_thread = collected.get("email_thread") or {}
ledger_search = collected.get("ledger_search") or {}
vendor_history = collected.get("vendor_history", []) or []
bank_compares = collected.get("bank_compares", [])
ledger_hits = collected.get("ledger_hits", [])
context = {
"task": "Task D - AP inbox incident triage (complex fraud)",
"invoice_records": invoice_records,
"email_thread": email_thread,
"ledger_search": ledger_search,
"vendor_history": vendor_history,
"bank_comparisons": bank_compares,
"ledger_hits": ledger_hits,
}
system_prompt = """You are a senior fraud investigator analyzing a complex AP case. Look for multiple fraud vectors:
Fraud indicators:
1. Bank account changes/mismatches
2. Duplicate invoice clusters
3. Email-based fraud (domain spoofing, policy bypass attempts)
4. Approval threshold evasion (splitting invoices)
5. Suspicious vendor history
Decision:
- PAY: Clean transaction, all checks pass
- ESCALATE_FRAUD: Any fraud indicators present
CRITICAL INSTRUCTIONS:
1. If you detect ANY fraud indicator, you MUST include it in reason_codes
2. The evidence_map and reason_codes MUST match - every reason needs evidence
3. Be thorough - check bank accounts, email headers, invoice amounts, duplicates
Return JSON format:
{
"decision": "PAY" or "ESCALATE_FRAUD",
"confidence": float (0.0-1.0),
"reason_codes": ["bank_override_attempt", "duplicate_near_match", "sender_domain_spoof", "policy_bypass_attempt", "approval_threshold_evasion"],
"policy_checks": {
"three_way_match": "pass" or "fail",
"bank_change_verification": "pass" or "fail",
"duplicate_check": "pass" or "fail",
"approval_threshold_check": "pass" or "fail"
},
"counterfactual": "What conditions would make this PAY instead of ESCALATE?",
"reasoning": "detailed analysis"
}"""
try:
response = client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": compact_json(context)},
],
temperature=TEMPERATURE,
max_completion_tokens=MAX_TOKENS,
)
content = response.choices[0].message.content or "{}"
track_api_usage(response.usage)
result = json.loads(content)
# Validate and fix the decision
validated = validate_and_fix_decision(result, collected, "task_d")
return validated
except Exception as e:
trace(f"[LLM ERROR] Task D: {e}")
return heuristic_task_d(collected)
def heuristic_task_d(collected: dict[str, Any]) -> dict[str, Any]:
"""Original deterministic logic as fallback."""
invoice_records = collected.get("invoice_records", []) or []
primary_record = invoice_records[0] if invoice_records else {"fields": collected.get("invoice_fields", {}), "evidence": collected.get("invoice_evidence", {})}
invoice_evidence = primary_record.get("evidence", {})
email_evidence = collected.get("email_evidence", {})
email_thread = collected.get("email_thread") or {}
ledger_search = collected.get("ledger_search") or {}
bank_compares = collected.get("bank_compares", [])
vendor_history = collected.get("vendor_history", []) or []
email_flags = {normalize_text(flag) for flag in email_thread.get("derived_flags", []) or email_thread.get("flags", []) or []}
ledger_hits = collected.get("ledger_hits", []) or []
duplicate_detected = bool(ledger_hits) or int(ledger_search.get("exact_duplicate_count", 0) or 0) > 0
bank_mismatch = any(compare and not bool(compare.get("matched")) for compare in bank_compares)
invoice_totals = [safe_float(record.get("fields", {}).get("total")) for record in invoice_records]
threshold_split = (len(invoice_totals) >= 2 and sum(invoice_totals) >= 3000.0 and all(0.0 < total < 2000.0 for total in invoice_totals))
suspicious_history = any(normalize_text(item.get("status")) in {"rejected", "pending_callback_verification", "failed", "denied"} and "bank" in normalize_text(item.get("change_type") or item.get("event_type")) for item in vendor_history)
suspicious = duplicate_detected or bank_mismatch or bool(email_flags) or suspicious_history or threshold_split
evidence_map = {}
reason_codes = []
bank_evidence = None
duplicate_evidence = None
for record in invoice_records:
evidence = record.get("evidence", {})
if bank_evidence is None and "bank_account" in evidence:
bank_evidence = evidence["bank_account"]
if duplicate_evidence is None and "invoice_number" in evidence:
duplicate_evidence = evidence["invoice_number"]
if bank_mismatch and bank_evidence:
evidence_map["bank_override_attempt"] = bank_evidence
reason_codes.append("bank_override_attempt")
if duplicate_detected and duplicate_evidence:
evidence_map["duplicate_near_match"] = duplicate_evidence
reason_codes.append("duplicate_near_match")
if ("sender_domain_spoof" in email_flags or "sender_domain_spoof" in email_evidence) and "from_header" in email_evidence:
evidence_map["sender_domain_spoof"] = email_evidence["from_header"]
reason_codes.append("sender_domain_spoof")
if "approval_threshold_evasion" in email_flags or threshold_split:
evidence_map["approval_threshold_evasion"] = email_evidence.get("approval_threshold_evasion") or email_evidence.get("subject_header") or duplicate_evidence
reason_codes.append("approval_threshold_evasion")
if "policy_bypass_attempt" in email_flags or "policy_bypass_attempt" in email_evidence:
evidence_map["policy_bypass_attempt"] = email_evidence.get("policy_bypass_attempt") or email_evidence.get("subject_header") or email_evidence.get("from_header")
reason_codes.append("policy_bypass_attempt")
if not suspicious:
return {
"decision": "PAY",
"confidence": 0.88,
"reason_codes": [],
"policy_checks": {"three_way_match": "pass", "bank_change_verification": "pass", "duplicate_check": "pass", "approval_threshold_check": "pass"},
"evidence_map": {},
"counterfactual": "Would HOLD if sender domain changed, bank account mismatched, or duplicate cluster appeared.",
}
checks = {"three_way_match": "pass", "bank_change_verification": "fail" if bank_mismatch or "sender_domain_spoof" in reason_codes or "policy_bypass_attempt" in reason_codes else "pass", "duplicate_check": "fail" if duplicate_detected else "pass"}
if "approval_threshold_evasion" in reason_codes:
checks["approval_threshold_check"] = "fail"
return {
"decision": "ESCALATE_FRAUD",
"confidence": 0.99,
"reason_codes": sorted(set(reason_codes)),
"policy_checks": checks,
"evidence_map": evidence_map,
"counterfactual": "Would PAY if all required policy checks passed.",
}
# =============================================================================
# MAIN INFERENCE LOGIC
# =============================================================================
def build_final_submission(task_type: str, collected: dict[str, Any], client: Optional[OpenAI]) -> dict[str, Any]:
"""Build final submission using LLM-powered decisions."""
if task_type == "task_a":
return {
"decision": "NEEDS_REVIEW",
"confidence": 0.90,
"extracted_fields": collected["invoice_fields"],
"line_items": collected["invoice_line_items"],
"evidence_map": collected["invoice_evidence"],
}
if task_type == "task_b":
return llm_decision_task_b(client, collected)
if task_type == "task_c":
return llm_decision_task_c(client, collected)
if task_type == "task_d":
return llm_decision_task_d(client, collected)
return {"decision": "NEEDS_REVIEW", "confidence": 0.50}
def perform_step(env: LedgerShieldEnv, step_no: int, rewards: list[float], action: LedgerShieldAction) -> tuple[Any, int]:
result = env.step(action)
reward = float(result.reward or 0.0)
rewards.append(reward)
tool_result = getattr(result.observation, "last_tool_result", {}) or {}
error = tool_result.get("error")
if error is None and result.info:
error = result.info.get("error")
log_step(step=step_no, action=format_action(action), reward=reward, done=bool(result.done), error=error)
return result, step_no + 1
def capture_invoice_data(collected: dict[str, Any], tool_result: dict[str, Any]) -> None:
doc_id = str(tool_result.get("doc_id", ""))
tokens = list(tool_result.get("tokens", []) or [])
fields, evidence, line_items = parse_invoice_tokens(tokens, doc_id)
record = {
"doc_id": doc_id,
"tokens": tokens,
"fields": fields,
"evidence": evidence,
"line_tokens": [token for token in tokens if "|" in str(token.get("text", ""))],
}
collected.setdefault("invoice_records", []).append(record)
collected["invoice_doc_id"] = doc_id
collected["invoice_tokens"] = tokens
collected["invoice_fields"] = fields
collected["invoice_evidence"] = evidence
collected["invoice_line_items"] = line_items
collected["invoice_line_tokens"] = record["line_tokens"]
def capture_email_data(collected: dict[str, Any], tool_result: dict[str, Any]) -> None:
doc_id = str(tool_result.get("doc_id", ""))
tokens = list(tool_result.get("tokens", []) or [])
collected["email_doc_id"] = doc_id
collected["email_tokens"] = tokens
collected["email_evidence"] = parse_email_tokens(tokens, doc_id)
def run_episode(env_url: str, case_id: str, client: Optional[OpenAI]) -> dict[str, Any]:
rewards: list[float] = []
steps_taken = 0
final_score = 0.0
success = False
task_type = "unknown"
env = LedgerShieldEnv(base_url=env_url)
log_start(task=case_id, env=BENCHMARK, model=MODEL_NAME)
try:
reset_result = env.reset(case_id=case_id)
observation = reset_result.observation
task_type = observation.task_type
step_no = 1
collected: dict[str, Any] = {
"invoice_doc_id": "", "invoice_tokens": [], "invoice_fields": {},
"invoice_evidence": {}, "invoice_line_items": [], "invoice_line_tokens": [],
"invoice_records": [], "email_doc_id": "", "email_tokens": [],
"email_evidence": {}, "po": None, "receipt": None, "ledger_hits": [],
"ledger_queries": {}, "ledger_search": {}, "vendor_history": [],
"email_thread": None, "bank_compare": None, "bank_compares": [],
}
invoice_doc_ids = []
email_doc_id = ""
for doc in observation.visible_documents:
doc_type = normalize_text(doc.get("doc_type"))
if doc_type == "invoice":
invoice_doc_ids.append(str(doc.get("doc_id")))
if doc_type == "email" and not email_doc_id:
email_doc_id = str(doc.get("doc_id"))
if not invoice_doc_ids:
raise RuntimeError(f"No visible invoice document for {case_id}")
for invoice_doc_id in invoice_doc_ids:
ocr_invoice_result, step_no = perform_step(
env, step_no, rewards,
LedgerShieldAction(action_type="ocr", payload={"doc_id": invoice_doc_id, "mode": "accurate"}),
)
steps_taken = step_no - 1
capture_invoice_data(collected, ocr_invoice_result.observation.last_tool_result)
if task_type != "task_d":
break
if task_type == "task_a":
invoice_doc_id = invoice_doc_ids[0]
zoom_result, step_no = perform_step(
env, step_no, rewards,
LedgerShieldAction(action_type="zoom", payload={"doc_id": invoice_doc_id, "bbox": [0, 0, 400, 400]}),
)
steps_taken = step_no - 1
if zoom_result.done:
final_score = float(zoom_result.info.get("final_score", rewards[-1] if rewards else 0.0))
success = final_score >= SUCCESS_SCORE_THRESHOLD
submit_payload = build_final_submission(task_type, collected, client)
final_result, step_no = perform_step(
env, step_no, rewards,
LedgerShieldAction(action_type="submit_decision", payload=submit_payload),
)
steps_taken = step_no - 1
final_score = float(final_result.info.get("final_score", final_result.reward or 0.0))
success = final_score >= SUCCESS_SCORE_THRESHOLD
return {"case_id": case_id, "task_type": task_type, "score": final_score, "steps": steps_taken}
invoice_fields = collected["invoice_fields"]
vendor_key = vendor_key_for(invoice_fields)
if not vendor_key:
vendor_key = normalize_text(collected.get("email_thread", {}).get("vendor_key"))
po_id = str(invoice_fields.get("po_id", "")).strip()
receipt_id = str(invoice_fields.get("receipt_id", "")).strip()
invoice_total = safe_float(invoice_fields.get("total"))
invoice_number = str(invoice_fields.get("invoice_number", "")).strip()
proposed_bank_account = str(invoice_fields.get("bank_account", "")).strip()
if task_type == "task_b":
action_plan = [
LedgerShieldAction(action_type="lookup_policy", payload={}),
LedgerShieldAction(action_type="lookup_po", payload={"po_id": po_id}),
LedgerShieldAction(action_type="lookup_receipt", payload={"receipt_id": receipt_id or po_id.replace("PO-", "GRN-")}),
]
elif task_type == "task_c":
action_plan = [
LedgerShieldAction(action_type="search_ledger", payload={"vendor_key": vendor_key, "invoice_number": invoice_number, "amount": invoice_total}),
LedgerShieldAction(action_type="compare_bank_account", payload={"vendor_key": vendor_key, "proposed_bank_account": proposed_bank_account}),
]
else:
action_plan = []
if email_doc_id:
action_plan.extend([
LedgerShieldAction(action_type="ocr", payload={"doc_id": email_doc_id, "mode": "accurate"}),
LedgerShieldAction(action_type="inspect_email_thread", payload={"thread_id": email_doc_id}),
])
action_plan.extend([
LedgerShieldAction(action_type="lookup_vendor_history", payload={"vendor_key": vendor_key}),
LedgerShieldAction(action_type="lookup_policy", payload={}),
])
for record in collected.get("invoice_records", []) or []:
record_fields = record.get("fields", {})
action_plan.append(LedgerShieldAction(action_type="compare_bank_account", payload={"vendor_key": vendor_key, "proposed_bank_account": str(record_fields.get("bank_account", "")).strip()}))
action_plan.append(LedgerShieldAction(action_type="search_ledger", payload={"vendor_key": vendor_key, "invoice_number": str(record_fields.get("invoice_number", "")).strip(), "amount": safe_float(record_fields.get("total"))}))
for action in action_plan:
if step_no > MAX_STEPS:
break
result, step_no = perform_step(env, step_no, rewards, action)
steps_taken = step_no - 1
tool = result.observation.last_tool_result or {}
tool_name = tool.get("tool_name")
if tool_name == "lookup_po" and tool.get("success"):
collected["po"] = tool.get("po")
elif tool_name == "lookup_receipt" and tool.get("success"):
collected["receipt"] = tool.get("receipt")
elif tool_name == "search_ledger" and tool.get("success"):
hits = list(tool.get("hits", []) or [])
existing_ids = {row.get("ledger_id") for row in collected["ledger_hits"]}
for hit in hits:
if hit.get("ledger_id") not in existing_ids:
collected["ledger_hits"].append(hit)
existing_ids.add(hit.get("ledger_id"))
invoice_key = normalize_text(action.payload.get("invoice_number"))
if invoice_key:
collected["ledger_queries"][invoice_key] = tool
collected["ledger_search"] = tool
elif tool_name == "lookup_vendor_history" and tool.get("success"):
collected["vendor_history"] = list(tool.get("history", []) or [])
elif tool_name == "inspect_email_thread" and tool.get("success"):
collected["email_thread"] = tool.get("thread") or {}
elif tool_name == "compare_bank_account" and tool.get("success"):
collected["bank_compare"] = tool
collected["bank_compares"].append(tool)
elif tool_name == "lookup_policy" and tool.get("success"):
collected["policies"] = list(tool.get("policies", []) or [])
elif tool_name == "ocr" and tool.get("success") and tool.get("doc_id") == email_doc_id:
capture_email_data(collected, tool)
if result.done:
final_score = float(result.info.get("final_score", result.reward or 0.0))
success = final_score >= SUCCESS_SCORE_THRESHOLD
return {"case_id": case_id, "task_type": task_type, "score": final_score, "steps": steps_taken}
submit_payload = build_final_submission(task_type, collected, client)
if task_type == "task_b" and submit_payload["decision"] == "HOLD" and step_no <= MAX_STEPS:
result, step_no = perform_step(env, step_no, rewards, LedgerShieldAction(action_type="request_callback_verification", payload={}))
steps_taken = step_no - 1
if result.done:
final_score = float(result.info.get("final_score", result.reward or 0.0))
success = final_score >= SUCCESS_SCORE_THRESHOLD
return {"case_id": case_id, "task_type": task_type, "score": final_score, "steps": steps_taken}
if task_type == "task_c" and submit_payload["decision"] == "ESCALATE_FRAUD":
for action in [LedgerShieldAction(action_type="request_callback_verification", payload={}), LedgerShieldAction(action_type="flag_duplicate_cluster_review", payload={}), LedgerShieldAction(action_type="route_to_security", payload={}), LedgerShieldAction(action_type="freeze_vendor_profile", payload={})]:
if step_no > MAX_STEPS:
break
result, step_no = perform_step(env, step_no, rewards, action)
steps_taken = step_no - 1
if result.done:
final_score = float(result.info.get("final_score", result.reward or 0.0))
success = final_score >= SUCCESS_SCORE_THRESHOLD
return {"case_id": case_id, "task_type": task_type, "score": final_score, "steps": steps_taken}
if task_type == "task_d" and submit_payload["decision"] == "ESCALATE_FRAUD":
for action in [LedgerShieldAction(action_type="request_callback_verification", payload={}), LedgerShieldAction(action_type="route_to_security", payload={}), LedgerShieldAction(action_type="freeze_vendor_profile", payload={})]:
if step_no > MAX_STEPS:
break
result, step_no = perform_step(env, step_no, rewards, action)
steps_taken = step_no - 1
if result.done:
final_score = float(result.info.get("final_score", result.reward or 0.0))
success = final_score >= SUCCESS_SCORE_THRESHOLD
return {"case_id": case_id, "task_type": task_type, "score": final_score, "steps": steps_taken}
if step_no <= MAX_STEPS:
final_result, step_no = perform_step(env, step_no, rewards, LedgerShieldAction(action_type="submit_decision", payload=submit_payload))
steps_taken = step_no - 1
final_score = float(final_result.info.get("final_score", final_result.reward or 0.0))
success = final_score >= SUCCESS_SCORE_THRESHOLD
else:
final_score = clamp(rewards[-1] if rewards else 0.0, 0.0, 1.0)
success = False
return {"case_id": case_id, "task_type": task_type, "score": final_score, "steps": steps_taken}
except Exception as exc:
trace(f"[ERROR] episode failed for {case_id}: {exc}")
return {"case_id": case_id, "task_type": task_type, "score": 0.0, "steps": steps_taken, "error": str(exc)}
finally:
try:
env.close()
except Exception as exc:
trace(f"[DEBUG] env.close failed for {case_id}: {exc}")
log_end(success=success, steps=steps_taken, rewards=rewards)
def build_openai_client() -> Optional[OpenAI]:
if not API_KEY:
trace("[DEBUG] OPENAI_API_KEY not set; running heuristic-only fallback.")
return None
try:
return OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
except Exception as exc:
trace(f"[DEBUG] failed to initialize OpenAI client: {exc}")
return None
def run_baseline_inference(env_url: str, cases: list[str]) -> dict[str, Any]:
client = build_openai_client()
results = [run_episode(env_url=env_url, case_id=case_id, client=client) for case_id in cases]
avg_score = sum(result.get("score", 0.0) for result in results) / max(len(results), 1)
trace(f"[SUMMARY] cases={len(results)} avg_score={avg_score:.4f} scores={compact_json({result['case_id']: result.get('score', 0.0) for result in results})}")
return {"results": results, "average_score": avg_score}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="LedgerShield improved LLM-powered inference with validation")
parser.add_argument("--api-url", default=API_BASE_URL)
parser.add_argument("--model", default=MODEL_NAME)
parser.add_argument("--token", default=API_KEY)
parser.add_argument("--env-url", default=ENV_URL)
parser.add_argument("--cases", nargs="+", default=DEFAULT_CASES)
return parser.parse_args()
def main() -> None:
global API_BASE_URL, MODEL_NAME, API_KEY
args = parse_args()
API_BASE_URL = args.api_url
MODEL_NAME = args.model
API_KEY = args.token
reset_api_tracking()
run_baseline_inference(env_url=args.env_url, cases=args.cases)
print_api_summary()
if __name__ == "__main__":
main()