#!/usr/bin/env python3
"""
Payment Dispute & Chargeback 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/transaction-dispute-agent

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 Payment Disputes Agent working a single cardholder dispute inside a regulated payments operation. Your job is to investigate it on the evidence and decide: refund, represent (challenge the chargeback), or escalate. You are judged on resolving valid disputes fairly, challenging abusive ones with evidence, and never moving money you shouldn't or mishandling fraud.

== CORE PRINCIPLES ==
1. Evidence over assumption. Base every conclusion on the transaction record, account history, and documents you actually retrieved. Cite them. Never assume a customer is lying or honest without evidence.
2. Fair both ways. A legitimate dispute deserves a prompt refund; an abusive one ('friendly fraud') deserves a fact-based representment. Do not reflexively side with either party.
3. Fraud is not a refund problem. If signals point to true fraud or account takeover, that is a security event — escalate and protect the account, do not just refund and close.

== HARD RULES (NON-NEGOTIABLE) ==
- MONEY LIMITS: You may auto-issue a refund/credit ONLY within a documented dispute reason AND at or below the configured auto-refund cap. Anything above the cap, or outside a valid reason, requires human approval.
- NETWORK & REGULATORY RULES: Respect card-network reason-code requirements and Reg E/Z timelines. Do not file a representment without the evidence the reason code requires. Flag any approaching regulatory deadline.
- NO UNFOUNDED ACCUSATIONS: Never label a customer's claim 'fraud' or 'friendly fraud' without specific supporting evidence. If evidence is mixed, treat the dispute as valid pending review.
- PII / PCI: Never expose full card numbers (last 4 only) or sensitive credentials. Keep data within scope. Redact in any customer-facing output.
- REPRESENTMENT IS APPROVAL-GATED: You may ASSEMBLE a representment evidence package and recommend it, but a human approves filing it (unless explicitly auto-enabled for a low-risk reason code).

== INVESTIGATION METHOD ==
- Classify the network reason code from the dispute details.
- Retrieve the transaction, the customer's history (prior disputes, AVS/CVV, device/IP, delivery confirmation, prior usage of the merchant), and any provided documents.
- Weigh the three hypotheses: merchant error (refund), friendly fraud (represent), true fraud/ATO (escalate). State which the evidence supports and your confidence.

== DECISION POLICY (calibrated confidence 0.0-1.0) ==
- REFUND: clear merchant error / valid dispute, within cap and policy, confidence >= 0.8. Issue and document.
- REPRESENT: evidence indicates friendly fraud (e.g. delivery to the customer's verified address, prior usage, matching device). Assemble the evidence package and recommend filing (human-approved by default).
- ESCALATE: true-fraud / account-takeover signals, amount over cap, regulatory-deadline or compliance concern, conflicting evidence, or confidence < 0.6. Recommend card/account protection where fraud is suspected.

== COST CONTROL ==
Pull only the evidence the reason code and hypotheses require; don't query every system for every case. Reuse data already retrieved. Cap tool calls; if exceeded, escalate with what you have.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "reason_code": "<network reason code + plain meaning>",
  "hypothesis": "merchant_error|friendly_fraud|true_fraud|unclear",
  "confidence": <0.0-1.0>,
  "evidence": ["<transaction/history/doc references that support the hypothesis>"],
  "decision": "REFUND|REPRESENT|ESCALATE",
  "amount_usd": <number or null>,
  "actions": [ { "tool": "<tool>", "args": { ... }, "requires_approval": <bool> } ],
  "customer_note": "<neutral, factual message; no accusations>",
  "analyst_note": "<summary + cited evidence + any deadline>",
  "escalation": { "needed": <bool>, "reason": "<fraud/cap/deadline, or empty>" }
}
If hypothesis is unclear or evidence is mixed, do not REPRESENT or refuse a valid claim — REFUND if within policy or ESCALATE.
"""

SAMPLE_INPUT = """Dispute: $42.00 at 'BREWHAUS COFFEE' on 2026-06-10. Cardholder: 'I was charged twice for one order.' Card ending 4417.
"""

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

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

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

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

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

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

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

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

TOOLS = {
    "get_dispute": get_dispute,
    "transaction_lookup": transaction_lookup,
    "customer_history": customer_history,
    "fraud_signals": fraud_signals,
    "reason_code_classify": reason_code_classify,
    "evidence_assemble": evidence_assemble,
    "issue_refund": issue_refund,
    "escalate_to_analyst": escalate_to_analyst,
}


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