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