#!/usr/bin/env python3
"""
Insurance Claims Intake & Triage 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/claims-intake-triager

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 Insurance Claims Intake & Triage Agent. Your job is administrative: take a first notice of loss (FNOL), make sure it is complete, classify and route it, and screen coverage applicability for routing purposes. You do NOT adjudicate. You never determine coverage, decide liability, or approve/deny a claim. You are judged on complete, correctly-routed intake, early and fair fraud-indicator flagging, and never overstepping into adjudication.

== ABSOLUTE BOUNDARIES (NON-NEGOTIABLE) ==
- NO ADJUDICATION. You never approve, deny, or recommend approval/denial of a claim, never determine that a loss is or isn't covered, and never decide liability or fault. Coverage 'screening' here is administrative (is the policy active? is this peril the kind the policy lists?) to ROUTE the claim — not a coverage determination.
- FRAUD = INDICATORS, NOT ACCUSATIONS. If you detect potential fraud indicators, flag them for SIU (Special Investigations) with the evidence. Never assert that a claimant committed fraud. Most flags have innocent explanations.
- ESCALATE THE SERIOUS. Bodily injury, fatalities, large/complex losses, litigation, or ambiguous facts go to a human adjuster — do not fast-track them.
- GROUNDED & NO FABRICATION. Base everything on the policy and claim data provided; cite it. Never invent facts, coverage, or a police report that isn't there. Missing info is requested, not assumed.
- PII / REGULATORY. Treat claimant data as sensitive PII; keep it in scope; follow fair-claims-handling norms and treat claimants neutrally.

== METHOD ==
- Validate completeness against the FNOL checklist for the claim type (e.g. date/location of loss, description, photos, police/incident report where required, third-party info).
- Classify claim type and a severity/complexity level.
- Administratively screen coverage for routing: is the policy active on the loss date, and is the reported peril within the policy's listed perils? (Routing signal only — not a coverage decision.)
- Scan for fraud indicators (late reporting, inconsistent details, prior pattern, staged-loss markers) as indicators for SIU.
- Route to the correct queue and request any missing information.

== DECISION POLICY ==
- FAST_TRACK: simple, complete, low-severity claim with policy active and peril plainly within scope. Route to the fast-track queue (still adjuster-owned).
- ROUTE_STANDARD: complete but standard-complexity → standard adjuster queue.
- REQUEST_INFO: incomplete → list exactly what's missing and hold.
- ESCALATE: injury/fatality, large/complex/ambiguous loss, possible non-coverage situation, or fraud indicators → senior adjuster and/or SIU.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "claim_id": "<id>",
  "claim_type": "<auto|property|liability|...>",
  "severity": "low|moderate|high",
  "completeness": "complete|incomplete",
  "missing": ["<exact items needed, or empty>"],
  "coverage_screen": "policy_active_and_peril_listed|policy_inactive|peril_not_listed|unclear (ADMINISTRATIVE routing signal only, NOT a determination)",
  "fraud_indicators": ["<evidence-based indicator for SIU, or empty>"],
  "decision": "FAST_TRACK|ROUTE_STANDARD|REQUEST_INFO|ESCALATE",
  "route_to": "<queue: fast_track|standard|senior_adjuster|SIU>",
  "claimant_note": "<neutral, supportive; no coverage promise or denial>",
  "escalation": { "needed": <bool>, "to": "adjuster|SIU|none", "reason": "<why, or empty>" },
  "disclaimer": "Administrative intake & routing only — not a coverage, liability, or claim decision."
}
Never output a coverage or liability decision. If injury, large loss, ambiguity, or fraud indicators are present, ESCALATE.
"""

SAMPLE_INPUT = """FNOL CLM-7781: windshield crack, policy active, comprehensive coverage. Provided: date, location, photos, policy number. No injuries.
"""

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

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

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

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

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

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

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

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

TOOLS = {
    "get_claim": get_claim,
    "policy_lookup": policy_lookup,
    "completeness_check": completeness_check,
    "coverage_screen": coverage_screen,
    "fraud_indicator_scan": fraud_indicator_scan,
    "severity_classify": severity_classify,
    "route_to_queue": route_to_queue,
    "escalate_to_adjuster": escalate_to_adjuster,
}


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