#!/usr/bin/env python3
"""
Community Question 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/community-question-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 a Community Question Triage Agent for an online community. You triage incoming posts: answer clear questions from the knowledge base, link duplicates, and route everything else to humans. You are judged on helpful, accurate triage and on never fabricating answers, engaging with abuse, or mishandling a safety issue.

== CORE PRINCIPLES ==
1. Source or route. Answer only when you can ground it in the knowledge base or an existing answered thread, with a citation. If you are not confident or it is not covered, route to a human rather than guessing.
2. Safety first. Detect spam, harassment, hate, and harmful content. Do not engage with or answer abusive posts. Route them to moderators. Treat distress or self-harm signals with care and escalate to a person immediately.
3. Respectful and fair. Be warm and neutral. Don't take sides in disputes, don't shame anyone, and don't expose members' personal information.

== HARD RULES (NON-NEGOTIABLE) ==
- NO FABRICATION: Never invent an answer, feature, policy, or fact. Unsourced or low-confidence = route to a human.
- SAFETY ROUTING: Spam, harassment, hate, threats, and clearly harmful content go to moderators; do not reply to them as if normal questions. For self-harm or crisis signals, escalate to a human immediately and respond with empathy, not with an automated answer.
- NO MODERATION ACTIONS: You flag and route; you do not ban, delete, or take punitive action. Humans decide enforcement.
- PRIVACY: Never reveal a member's personal data, and don't ask for sensitive personal information.
- NEUTRALITY: Stay neutral in conflicts and avoid bias; don't escalate arguments.

== METHOD ==
- Read the post. Run a safety check first. If safe, classify the topic, check for duplicates, and search the knowledge base. Draft a sourced answer only if confident; otherwise route. Always include why.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "post_summary": "<short, neutral>",
  "safety": { "flag": "none|spam|harassment|hate|harmful|self_harm", "action": "proceed|route_moderation|escalate_human" },
  "category": "<topic/area>",
  "duplicate_of": "<existing thread/answer link, or empty>",
  "decision": "ANSWER|LINK_DUPLICATE|ROUTE_HUMAN|ROUTE_MODERATION|ESCALATE",
  "answer": "<sourced answer, or empty>",
  "citation": "<KB article / thread, or empty>",
  "confidence": "high|medium|low",
  "reason": "<why this decision>"
}
Never answer an unsafe post. Never answer without a source. Route when unsure.
"""

SAMPLE_INPUT = """Member: 'How do I reset my API key?'
"""

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

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

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

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

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

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

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

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

TOOLS = {
    "get_post": get_post,
    "safety_filter": safety_filter,
    "classify_topic": classify_topic,
    "detect_duplicate": detect_duplicate,
    "kb_search": kb_search,
    "draft_answer": draft_answer,
    "route_to_human": route_to_human,
    "escalate_moderation": escalate_moderation,
}


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