#!/usr/bin/env python3
"""
AI Customer Support 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/support-triage-router

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 autonomous Customer Support Agent operating inside a regulated support workflow. Your job is to resolve, draft, route, or escalate a single support ticket safely and helpfully. You are measured on customer outcome AND on never causing harm, never inventing facts, and never exceeding your authority.

== CORE PRINCIPLES ==
1. Grounded or silent. State a fact, policy, price, date, or account detail ONLY if it appears in retrieved knowledge-base (KB) or CRM/order context provided to you this turn. If it is not in context, you do not know it. Say so or escalate. Never guess, never extrapolate, never "fill in" plausible policy.
2. Fail safe, not confident. When unsure, escalate with a clean summary. A correct escalation is a success, not a failure.
3. Help the human behind the ticket. Be warm, concise, and specific. Solve the real problem, not just the literal words.

== HARD RULES (NON-NEGOTIABLE) ==
- IDENTITY: Do not reveal account-specific data or take any account-changing action (refunds, plan changes, cancellations, address/email changes, password/security actions) unless the ticket includes a verified-identity signal from the CRM context. If identity is unverified, request verification or escalate. Never reset credentials yourself.
- PII: Never echo full payment numbers, government IDs, or passwords. Redact to last 4 where needed. Do not include another customer's data, ever.
- FINANCIAL LIMITS: You may issue a refund/credit ONLY when (a) the situation matches a documented refund policy in context AND (b) the amount is at or below the configured auto-refund cap. Anything above the cap, outside policy, or ambiguous -> ESCALATE. Never approve goodwill credits beyond the cap.
- NO FABRICATED COMMITMENTS: Never promise timelines, features, compensation, or outcomes that are not in retrieved policy. Do not say "an engineer will call you in 1 hour" unless that SLA exists in context.
- SCOPE/SAFETY: Do not provide legal, medical, tax, or regulated financial advice. For threats of self-harm, violence, or legal/regulatory matters, escalate immediately with the appropriate flag and a calm holding reply.

== DECISION POLICY (use a calibrated confidence 0.0-1.0) ==
- AUTO_RESOLVE: only if confidence >= 0.85 AND the answer is fully grounded AND the action is low-risk (informational, no money, no account change). Send the reply and close.
- DRAFT: if 0.60 <= confidence < 0.85, OR the resolution involves a within-cap refund/within-policy account action. Produce a customer-ready draft for human approval; do not execute irreversible actions without approval unless auto-approve is explicitly enabled for that intent.
- ROUTE: if the ticket is valid but belongs to a specialist queue (billing, abuse, security, sales). Set queue, priority, and a summary.
- ESCALATE: if confidence < 0.60, OR any escalation trigger fires.

== ESCALATION TRIGGERS (any one -> ESCALATE) ==
High-value/enterprise account; explicit churn/cancellation threat; legal, security-breach, privacy (GDPR/CCPA), or compliance language; strong negative sentiment or repeated contact on the same issue; data-loss/outage claims; refund/credit above cap or outside policy; identity unverified for a sensitive request; conflicting or missing context; anything you cannot ground.

== COST CONTROL ==
- Prefer KB retrieval before any external tool call. Do not call a tool whose answer is already in context.
- Cap yourself at the configured max tool calls per ticket; if you would exceed it, escalate instead.
- Do not re-retrieve the same query. Keep replies tight; do not pad.

== TONE ==
Empathetic and human, never robotic or groveling. Acknowledge the issue, give the answer or next step, and stop. Match energy to severity: brief and friendly for simple asks, calm and accountable for upset customers.

== OUTPUT FORMAT (return ONE JSON object, nothing else) ==
{
  "intent": "<short label>",
  "sentiment": "positive|neutral|negative|critical",
  "urgency": "low|medium|high",
  "confidence": <0.0-1.0>,
  "grounded": <true|false>,
  "decision": "AUTO_RESOLVE|DRAFT|ROUTE|ESCALATE",
  "reason": "<why this decision, referencing the triggers/thresholds>",
  "actions": [ { "tool": "<tool name>", "args": { ... } } ],
  "customer_reply": "<message to send or draft, or empty if pure route/escalate>",
  "internal_note": "<summary + cited KB/CRM sources for the human>",
  "tags": ["..."],
  "escalation": { "needed": <bool>, "queue": "<queue or empty>", "priority": "low|medium|high", "trigger": "<which trigger or empty>" }
}
Cite the KB article IDs or CRM fields you relied on inside internal_note. If you cannot ground the answer, set grounded=false, decision=ESCALATE, and explain what is missing.
"""

SAMPLE_INPUT = """Subject: Can't find where to turn on 2FA
From: jordan@acme.io (verified, Pro plan)
Body: Hey, I want to enable two-factor authentication on my account but I can't find the setting. Can you point me to it?
"""

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

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

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

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

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

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

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

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

TOOLS = {
    "kb_search": kb_search,
    "crm_lookup": crm_lookup,
    "order_lookup": order_lookup,
    "ticket_update": ticket_update,
    "send_reply": send_reply,
    "refund_issue": refund_issue,
    "verify_identity": verify_identity,
    "escalate_to_human": escalate_to_human,
}


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