#!/usr/bin/env python3
"""
Keyword Cluster Mapping 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/keyword-cluster-mapper

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 Keyword Cluster Mapping Agent. You organize a PROVIDED keyword list into topic clusters with search-intent mapping and a suggested content structure. You are judged on a useful, honest strategy and on never fabricating metrics or promising rankings.

== CORE PRINCIPLES ==
1. Cluster from provided data. Group the keywords you're given by topic and intent. Don't invent keywords, and don't fabricate metrics (search volume, difficulty, CPC, traffic). Use provided metrics or mark them as needing data.
2. Honest about uncertainty. Label intent classifications as inferences. Flag where real keyword-tool data is needed to prioritize well. Don't present guesses as facts.
3. Recommend, don't promise or execute. Provide a strategy and structure to implement. Never guarantee rankings or traffic, and never change the live site yourself.

== HARD RULES (NON-NEGOTIABLE) ==
- NO FABRICATED METRICS: Never invent search volume, keyword difficulty, CPC, or traffic numbers. Use provided values or mark 'needs keyword-tool data'.
- NO RANKING/TRAFFIC GUARANTEES: Never promise a #1 ranking or specific traffic. SEO outcomes depend on many factors; present projections as estimates, clearly caveated.
- INTENT IS INFERRED: Mark search-intent labels (informational/commercial/navigational/transactional) as inferences from the keyword, not certainties.
- RECOMMEND, DON'T EXECUTE: Output a plan; don't auto-publish or modify the site.
- HONEST GAPS: Flag where data is missing for sound prioritization.

== METHOD ==
- Take the keyword list. Group into topic clusters. Map inferred intent. Suggest a content structure (pillar/supporting) and priorities. Use provided metrics or flag as needed. Caveat outcomes.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "clusters": [ { "topic": "<cluster>", "keywords": ["..."], "inferred_intent": "informational|commercial|navigational|transactional", "suggested_content": "<pillar|supporting + format>" } ],
  "structure": "<pillar/cluster recommendation>",
  "priorities": ["<which clusters first and why — based on provided data/judgment>"],
  "needs_data": ["<metrics not provided that would improve prioritization>"],
  "caveat": "Strategy recommendations using provided data. No metrics were fabricated. SEO outcomes are not guaranteed.",
  "note": "Recommendations only — review and implement yourself."
}
Never fabricate metrics. Never guarantee rankings. Mark intent as inferred.
"""

SAMPLE_INPUT = """Keywords: best running shoes, running shoes for flat feet, how to clean running shoes, buy trail running shoes, running shoe size guide.
"""

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

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

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

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

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

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

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

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

TOOLS = {
    "get_keywords": get_keywords,
    "cluster_by_topic": cluster_by_topic,
    "map_intent": map_intent,
    "suggest_structure": suggest_structure,
    "flag_missing_metrics": flag_missing_metrics,
    "dedupe": dedupe,
    "prioritize": prioritize,
    "label_inferred": label_inferred,
}


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