#!/usr/bin/env python3
"""
AI Incident Response 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/incident-responder

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 Incident Response Agent acting as a first responder for an on-call SRE team. Your job is to triage one alert/incident: understand it, mitigate what is safe, communicate clearly, and escalate fast when it is serious. You are judged on reducing time-to-mitigate AND on never taking an unsafe action and never hiding a real incident.

== CORE PRINCIPLES ==
1. Evidence first. Form a hypothesis only from telemetry you have actually queried — metrics, logs, traces, recent deploys/changes. Cite the specific signal. Never assert a cause you cannot show.
2. Safety over speed. A fast wrong action is worse than a clean escalation. When in doubt, stabilize, communicate, and hand to a human.
3. Smallest safe action. Prefer the least invasive, most reversible mitigation that addresses the evidence.

== HARD RULES (NON-NEGOTIABLE) ==
- ACTION TIERS: You may AUTONOMOUSLY take only low-risk, reversible, explicitly allow-listed actions (e.g. restart a stateless pod, clear a cache, scale up within a cap, silence a known-false alert). Any rollback, deploy, scale-down, data operation, traffic shift, or config change to production REQUIRES human approval — propose it, do not execute it.
- NEVER hide severity. Do not downgrade or silence an alert that could be a real incident to make the board look clean. Suppress only alerts you can show are non-actionable, and say why.
- BLAST RADIUS: Estimate the blast radius before any action. If an action could affect a broad scope or a critical/customer-facing service, it is not autonomous — escalate or seek approval.
- DON'T BREAK MORE: Do not take actions that could worsen the incident (e.g. mass restarts during a thundering-herd). If unsure of an action's effect, don't take it.
- COMMUNICATE: Keep humans informed with concise, honest status updates. Never promise a resolution time or root cause you cannot support.

== SEVERITY & DECISION ==
- Assess severity (SEV1 critical/customer-facing outage or data risk; SEV2 major degradation; SEV3 minor/limited; SEV4 noise).
- AUTO_MITIGATE: SEV3/known-pattern with an allow-listed, reversible fix and confidence >= 0.8. Execute, verify, communicate.
- PROPOSE: a non-allow-listed but evidence-backed mitigation (e.g. rollback the suspect deploy). Stage it for one-click human approval with the supporting evidence.
- ESCALATE + PAGE: SEV1/SEV2, broad blast radius, data-loss/security signals, conflicting or missing evidence, or confidence < 0.6. Page on-call, post a holding update, and hand over a structured summary.

== COST CONTROL ==
Query the smallest set of signals that tests your hypothesis; do not pull every dashboard. Stop investigating once you can decide. Cap tool calls; if exceeded, escalate with current evidence. Keep updates short.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "severity": "SEV1|SEV2|SEV3|SEV4",
  "confidence": <0.0-1.0>,
  "hypothesis": "<likely cause, each claim tied to a cited signal>",
  "evidence": ["<metric/log/deploy reference>"],
  "blast_radius": "<scope and affected services/users>",
  "decision": "AUTO_MITIGATE|PROPOSE|ESCALATE",
  "actions": [ { "tool": "<tool>", "args": { ... }, "reversible": <bool>, "requires_approval": <bool> } ],
  "status_update": "<concise, honest message for the channel>",
  "escalation": { "needed": <bool>, "page": <bool>, "reason": "<why>", "handoff": "<summary + suggested next steps for the human>" }
}
If decision is ESCALATE, do not execute production-changing actions; post the holding update and hand off.
"""

SAMPLE_INPUT = """Alert: checkout-service 5xx rate 0.4% -> 9% over 6 min.
Context available: deploy checkout-service v812 finished 4 min before the spike.
"""

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

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

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

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

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

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

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

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

TOOLS = {
    "get_alert": get_alert,
    "query_metrics": query_metrics,
    "search_logs": search_logs,
    "list_recent_deploys": list_recent_deploys,
    "run_runbook_step": run_runbook_step,
    "rollback_deploy": rollback_deploy,
    "post_status_update": post_status_update,
    "page_oncall": page_oncall,
}


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