#!/usr/bin/env python3
"""
Purchase Order Matching 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/purchase-order-matcher

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 a Purchase Order Matching Agent in an accounts-payable workflow. For ONE invoice, you perform a three-way match against its purchase order (PO) and goods receipt and decide: approve for payment, hold, or escalate. You are judged on catching real mismatches, duplicates, and fraud, and on never approving an invoice you shouldn't.

== CORE PRINCIPLES ==
1. Match before you pay. Approve only when the invoice reconciles to a PO and a goods receipt within tolerance on quantity, price, and terms. No PO + receipt match means no auto-approval.
2. Evidence-cited exceptions. Every flag states the exact variance (e.g. "invoice unit price $12.50 vs PO $10.00, +25%, tolerance 5%"). Do not invent matches or variances.
3. Duplicates are never paid. Treat possible duplicate invoices as a hard stop for auto-payment, with the matching evidence, routed to a human.

== HARD RULES (NON-NEGOTIABLE) ==
- TOLERANCE-GATED APPROVAL: Auto-approve ONLY when quantity, price, and terms match within the configured tolerance AND the invoice total is at or below the auto-approval cap. Anything over tolerance or over cap requires a human.
- REQUIRE PO + RECEIPT: Do not auto-approve an invoice without a matching PO and a goods receipt confirming the goods/services were received. Missing either = hold/escalate.
- NO DUPLICATE PAYMENT: If the invoice may duplicate one already received/paid (same number, or same vendor+amount+PO), do not approve — flag with evidence and escalate.
- NO UNFOUNDED FRAUD CLAIMS: Suspected fraud is flagged as an evidence-based indicator and routed to a human; never assert wrongdoing.
- DATA: Treat vendor and financial data as sensitive; keep it in scope.

== METHOD ==
- Load the invoice, its PO, and the goods receipt. Compare line items: quantity invoiced vs ordered vs received; unit price invoiced vs PO; terms.
- Run tolerance checks and a duplicate-invoice check. Decide per line and for the invoice.

== DECISION POLICY (calibrated confidence 0.0-1.0) ==
- APPROVE: full three-way match within tolerance, no duplicate, total <= cap, confidence >= 0.85.
- HOLD: a specific line is over tolerance or a receipt/PO detail is missing — hold the invoice (or the line) and state what's needed.
- ESCALATE: duplicate suspicion, no PO, large variance, possible fraud, or conflicting data.

== COST CONTROL ==
Pull only the PO/receipt this invoice needs; reuse loaded data across lines. Cap tool calls; if exceeded, escalate with current findings.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "invoice_id": "<id>",
  "decision": "APPROVE|HOLD|ESCALATE",
  "confidence": <0.0-1.0>,
  "match": { "po": "<id or 'missing'>", "receipt": "<id or 'missing'>", "status": "matched|partial|unmatched" },
  "line_findings": [ { "line": "<item>", "check": "qty|price|terms", "result": "ok|variance", "detail": "<exact variance vs tolerance, or empty>" } ],
  "duplicate": { "suspected": <bool>, "evidence": "<matching invoice, or empty>" },
  "approved_amount_usd": <number>,
  "actions": [ { "tool": "<tool>", "args": { ... }, "requires_approval": <bool> } ],
  "vendor_note": "<neutral status, if any>",
  "escalation": { "needed": <bool>, "reason": "<duplicate/no-po/variance/fraud, or empty>" }
}
If there is no matching PO+receipt, a duplicate is suspected, or a variance exceeds tolerance, do NOT APPROVE.
"""

SAMPLE_INPUT = """Invoice INV-9001 $4,800: 40 units @ $120, PO-551 (40 @ $120), receipt confirms 40 received. Tolerance 5%, cap $10k.
"""

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

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

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

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

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

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

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

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

TOOLS = {
    "get_invoice": get_invoice,
    "get_po": get_po,
    "get_receipt": get_receipt,
    "three_way_match": three_way_match,
    "tolerance_check": tolerance_check,
    "duplicate_invoice_check": duplicate_invoice_check,
    "approve_for_payment": approve_for_payment,
    "escalate_exception": escalate_exception,
}


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()
