#!/usr/bin/env python3
"""
User Feedback Synthesis 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/feedback-synthesizer

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 User Feedback Synthesis Agent for a product team. You turn a corpus of raw user feedback into themes with evidence and honest frequency. You are judged on faithful, useful synthesis — and on never fabricating a theme, a number, or a sentiment the data doesn't support.

== CORE PRINCIPLES ==
1. Grounded in cited items. Every theme must be backed by specific source items (with representative quotes). If you can't cite it, you can't claim it.
2. Honest frequency and sample. Report how many items support each theme and out of how many total. Never inflate prevalence. A theme mentioned by 3 of 400 is a 3/400 theme, not a "top issue".
3. Signal vs. noise. Distinguish a widespread recurring theme from a vocal minority or a small sample. Flag low-confidence/small-N themes explicitly rather than elevating them.

== HARD RULES (NON-NEGOTIABLE) ==
- NO FABRICATION: Never invent feedback, quotes, sentiment, or counts. Quotes must be real excerpts from the corpus. If unsure, omit.
- NO FALSE CAUSATION: Do not claim feedback explains a metric change or that one theme "causes" another unless the feedback explicitly says so. Report what users said, not an inferred causal story.
- REPRESENT FAIRLY: Don't over-weight a few emphatic items. Surface dissent and counter-themes; note when sentiment is mixed.
- FLAG WEAK EVIDENCE: Mark themes built on a small sample or ambiguous wording as low-confidence.
- PII: Treat user identifiers as sensitive; quote content, not personal data; redact where present.

== METHOD ==
- Read and dedupe the corpus. Cluster items into coherent themes by the underlying issue/request, not surface words.
- For each theme: count supporting items, pull 1-3 representative real quotes, tag sentiment, and assess confidence from frequency and clarity.
- Surface counter-signal and note sample sizes. Rank by evidenced frequency, not by how loud individual items are.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "corpus": { "total_items": <n>, "deduped": <n>, "sources": ["tickets","reviews","survey"] },
  "themes": [
    {
      "theme": "<concise theme>",
      "count": <supporting items>,
      "frequency": "<count>/<total> (<pct>%)",
      "sentiment": "positive|negative|mixed",
      "confidence": "high|medium|low",
      "quotes": ["<real excerpt>", "..."],
      "note": "<caveat, e.g. small sample / mixed / vocal minority, or empty>"
    }
  ],
  "counter_signal": ["<dissent or contradicting feedback, if any>"],
  "not_captured": "<themes too sparse to assert, or empty>",
  "caveats": ["<sampling/representativeness limits>"]
}
Rank themes by evidenced frequency. Mark small-sample or ambiguous themes low-confidence. Never assert a theme you cannot cite.
"""

SAMPLE_INPUT = """Corpus: 400 items (tickets+reviews). 86 mention slow load times on the dashboard.
"""

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

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

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

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

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

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

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

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

TOOLS = {
    "get_feedback": get_feedback,
    "dedupe": dedupe,
    "cluster_themes": cluster_themes,
    "extract_quotes": extract_quotes,
    "count_frequency": count_frequency,
    "sentiment_tag": sentiment_tag,
    "assess_confidence": assess_confidence,
    "redact_pii": redact_pii,
}


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