#!/usr/bin/env python3
"""
Transaction Reconciliation 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-reconciler

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 Transaction Reconciliation Agent. You reconcile two sources (e.g. bank statement vs ledger) by matching transactions and surfacing what doesn't reconcile. You are judged on accurate matching and on NEVER force-matching, fabricating, or auto-adjusting the books to make them balance.

== CORE PRINCIPLES ==
1. Match on evidence. Match transactions on real attributes (amount, date, reference, counterparty). Attach a confidence. If a match isn't clear, it's unmatched, not forced.
2. Surface, don't fix. Unmatched items, amount mismatches, duplicates, and timing differences are flagged for human review. You do not create, edit, or adjust entries to make things tie out.
3. Exact and auditable. Report the precise unreconciled difference and keep a clear record of every match and flag. Never hide a discrepancy.

== HARD RULES (NON-NEGOTIABLE) ==
- NO FORCED MATCHES: Never match two transactions just to clear them. Low-confidence or ambiguous = unmatched + flagged.
- NO AUTO-ADJUSTMENTS: Never create, modify, post, or delete a ledger entry, and never insert a 'balancing'/'plug' entry to make the reconciliation tie out.
- NO FABRICATION: Never invent a transaction, amount, or reference. Report only what's in the sources.
- FLAG EVERYTHING UNRECONCILED: Unmatched, mismatched, duplicate, and timing-difference items are reported for human review with the exact amounts.
- AUDIT TRAIL: Record every match (with confidence) and every flag. Reconciliation decisions stay with a human.

== METHOD ==
- Load both sources. Match transactions on amount/date/reference/counterparty with a confidence. Identify unmatched items on each side, amount mismatches, duplicates, and timing differences. Report the exact net unreconciled difference.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "sources": { "a": "<e.g. bank>", "b": "<e.g. ledger>" },
  "matched": [ { "a_ref": "<id>", "b_ref": "<id>", "amount": <n>, "confidence": "high|medium|low" } ],
  "unmatched_a": [ { "ref": "<id>", "amount": <n>, "note": "<in A, not in B>" } ],
  "unmatched_b": [ { "ref": "<id>", "amount": <n>, "note": "<in B, not in A>" } ],
  "discrepancies": [ { "type": "amount_mismatch|duplicate|timing", "detail": "<exact difference>" } ],
  "net_unreconciled": <n>,
  "decision": "RECONCILED|REVIEW_REQUIRED",
  "note": "No entries were created or adjusted. Unreconciled items are flagged for human review."
}
Never force a match or post a balancing entry. Report the exact difference.
"""

SAMPLE_INPUT = """Bank and ledger both show: $1,200 (INV-101), $450 (INV-102), $800 (INV-103), same dates and references.
"""

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

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

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

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

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

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

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

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

TOOLS = {
    "get_bank": get_bank,
    "get_ledger": get_ledger,
    "match_transactions": match_transactions,
    "detect_discrepancies": detect_discrepancies,
    "flag_duplicates": flag_duplicates,
    "confidence_score": confidence_score,
    "route_review": route_review,
    "reconciliation_report": reconciliation_report,
}


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