#!/usr/bin/env python3
"""
Expense Audit & Compliance Agent — runnable demo (Agent Blueprint from AgentKits)

This runs a REAL agent loop: it sends the kit's actual system prompt and a
realistic sample input to the model, prints the structured decision, and then
executes any proposed actions against MOCK tools (labelled [MOCK]).

The model/reasoning is real. The tools are stubs that return fake data and do
NOT touch any real system. Wiring the mock tools to your own integrations
(CRM, database, GitHub, etc.) is the final implementation step — see README.md.

Works with Anthropic OR OpenAI — it auto-detects whichever API key is set.

© 2026 AgentKits · www.agent-kits.com · Licensed CC BY 4.0 (attribution required)
Original blueprint: https://agent-kits.com/kit/expense-report-auditor

Setup (pick one):
    pip install anthropic   &&  export ANTHROPIC_API_KEY=sk-ant-...
    pip install openai      &&  export OPENAI_API_KEY=sk-...
Then:
    python run.py
"""
import os, json, sys

ANTHROPIC_MODEL = "claude-sonnet-4-6"
OPENAI_MODEL = "gpt-4o"

SYSTEM_PROMPT = """You are an Expense Audit Agent in a finance operation. You audit ONE expense report against the company's written policy and decide: approve, hold specific items, reject, or escalate. You are judged on catching real policy violations and fraud, fairness and accuracy, and never approving spend you shouldn't or accusing someone without evidence.

== CORE PRINCIPLES ==
1. Policy-grounded. Every flag must cite the specific policy rule it violates (limit, category, receipt requirement, per-diem). Do not invent rules or violations; if the policy is silent, it is not a violation.
2. Evidence over suspicion. Base duplicate/fraud flags on concrete evidence (matching receipt, overlapping dates, identical amounts). Never label an employee 'fraud' without cited evidence; flag patterns for human review instead.
3. Audit each line. Approve the compliant items and flag only the specific non-compliant ones — don't reject a whole report over one bad line.

== HARD RULES (NON-NEGOTIABLE) ==
- APPROVAL LIMITS: Auto-approve ONLY when every line is within policy, required receipts are present, and the total is at or below the configured auto-approval cap. Anything above the cap, or with a policy exception, requires human approval.
- RECEIPTS REQUIRED: Do not approve an item that policy requires a receipt for if the receipt is missing or unreadable — hold it.
- NO UNFOUNDED ACCUSATIONS: Suspected duplicates/fraud are flagged with the evidence and routed to a human; never assert intent or wrongdoing.
- PII/DATA: Treat employee and financial data as sensitive; keep it in scope; redact where not needed.
- FAIRNESS: Apply the same policy consistently to every report.

== METHOD ==
- Load the report and the applicable policy. For each line: check category, amount vs. limit, receipt presence/validity, and per-diem/date rules.
- Run duplicate detection (same amount+date+merchant, or the same receipt across reports) and basic anomaly checks (e.g. mileage + flight for the same leg, weekend/personal patterns).
- Decide per line: ok / flag (with rule cited) / hold (missing doc). Then decide the report outcome.

== DECISION POLICY (calibrated confidence 0.0-1.0) ==
- APPROVE: all lines compliant, receipts present, total <= cap, confidence >= 0.85.
- HOLD: specific items missing receipts or needing a minor fix — approve the rest, hold those.
- REJECT_WITH_REASONS: clear policy violations; cite each. (Recommendation for a human to confirm.)
- ESCALATE: total over cap, suspected duplicate/fraud, policy exception, or conflicting evidence.

== COST CONTROL ==
Check only what each line needs; reuse the policy already loaded. Cap tool calls; if exceeded, approve the clearly-clean lines and escalate the rest.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "report_id": "<id>",
  "decision": "APPROVE|HOLD|REJECT_WITH_REASONS|ESCALATE",
  "confidence": <0.0-1.0>,
  "total_usd": <number>,
  "line_findings": [ { "item": "<line>", "status": "ok|flag|hold", "rule": "<policy rule cited, or empty>", "note": "<short>" } ],
  "fraud_signals": ["<evidence-based pattern, or empty>"],
  "approved_amount_usd": <number>,
  "actions": [ { "tool": "<tool>", "args": { ... }, "requires_approval": <bool> } ],
  "employee_note": "<neutral, factual; no accusation>",
  "escalation": { "needed": <bool>, "reason": "<cap/fraud/exception, or empty>" }
}
If evidence is mixed, prefer HOLD or ESCALATE over REJECT, and never accuse without cited evidence.
"""

SAMPLE_INPUT = """Report EXP-3310, total $214: hotel $180 (receipt), taxi $19 (receipt), lunch $15 (receipt). Policy: hotel<=300/night, meals<=60, receipts over $25.
"""

# ---- MOCK tools (stubs — replace with real integrations to deploy) ----
def get_report(args=None):
    print(f"  [MOCK] get_report(" + str(args) + ")")
    return {"status": "mock", "tool": "get_report"}

def policy_lookup(args=None):
    print(f"  [MOCK] policy_lookup(" + str(args) + ")")
    return {"status": "mock", "tool": "policy_lookup"}

def receipt_verify(args=None):
    print(f"  [MOCK] receipt_verify(" + str(args) + ")")
    return {"status": "mock", "tool": "receipt_verify"}

def duplicate_check(args=None):
    print(f"  [MOCK] duplicate_check(" + str(args) + ")")
    return {"status": "mock", "tool": "duplicate_check"}

def fraud_signals(args=None):
    print(f"  [MOCK] fraud_signals(" + str(args) + ")")
    return {"status": "mock", "tool": "fraud_signals"}

def categorize(args=None):
    print(f"  [MOCK] categorize(" + str(args) + ")")
    return {"status": "mock", "tool": "categorize"}

def approve_expense(args=None):
    print(f"  [MOCK] approve_expense(" + str(args) + ")")
    return {"status": "mock", "tool": "approve_expense"}

def escalate_to_finance(args=None):
    print(f"  [MOCK] escalate_to_finance(" + str(args) + ")")
    return {"status": "mock", "tool": "escalate_to_finance"}

TOOLS = {
    "get_report": get_report,
    "policy_lookup": policy_lookup,
    "receipt_verify": receipt_verify,
    "duplicate_check": duplicate_check,
    "fraud_signals": fraud_signals,
    "categorize": categorize,
    "approve_expense": approve_expense,
    "escalate_to_finance": escalate_to_finance,
}


def call_model(system, user):
    """Dispatch to whichever provider's key is configured."""
    if os.environ.get("ANTHROPIC_API_KEY"):
        try:
            from anthropic import Anthropic
        except ImportError:
            sys.exit("Run: pip install anthropic")
        msg = Anthropic().messages.create(
            model=ANTHROPIC_MODEL,
            max_tokens=1600,
            system=system,
            messages=[{"role": "user", "content": user}],
        )
        return "".join(b.text for b in msg.content if getattr(b, "type", "") == "text")
    if os.environ.get("OPENAI_API_KEY"):
        try:
            from openai import OpenAI
        except ImportError:
            sys.exit("Run: pip install openai")
        resp = OpenAI().chat.completions.create(
            model=OPENAI_MODEL,
            max_tokens=1600,
            messages=[
                {"role": "system", "content": system},
                {"role": "user", "content": user},
            ],
        )
        return resp.choices[0].message.content
    sys.exit("Set ANTHROPIC_API_KEY or OPENAI_API_KEY first.")


def extract_json(text):
    a, b = text.find("{"), text.rfind("}")
    if a == -1 or b == -1:
        return None
    try:
        return json.loads(text[a : b + 1])
    except json.JSONDecodeError:
        return None


def main():
    print("=" * 70)
    print("INPUT:\n" + SAMPLE_INPUT)
    print("=" * 70)

    text = call_model(SYSTEM_PROMPT, SAMPLE_INPUT)
    print("AGENT DECISION:\n" + text)
    print("=" * 70)

    data = extract_json(text)
    if not data:
        print("(No JSON object parsed — the model returned prose.)")
        return

    actions = data.get("actions") or []
    if actions:
        print("EXECUTING PROPOSED ACTIONS (mock):")
        for a in actions:
            name = a.get("tool")
            args = a.get("args", {})
            if name in TOOLS:
                TOOLS[name](args)
            else:
                print(f"  [skip] no mock tool for '{name}'")
    else:
        print("This blueprint returned a structured result (no side-effecting "
              "actions in this sample). Available mock tools: "
              + ", ".join(TOOLS.keys()))


if __name__ == "__main__":
    main()
