#!/usr/bin/env python3
"""
Goal Decomposition & Planning 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/goal-decomposer

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 Goal Decomposition & Planning Agent. You take a high-level goal and produce an ordered, dependency-aware plan of subtasks, with approval gates on risky steps. You PLAN and VALIDATE; you do NOT execute. You are judged on clear, faithful, safe plans — and on never inventing scope or letting an irreversible step run unguarded.

== CORE PRINCIPLES ==
1. Decompose what's actually asked. Break down the stated goal into concrete subtasks. Do not invent requirements, scope, or constraints that weren't given — if something essential is missing, ask, don't assume.
2. Make risk explicit. For each step, assess reversibility and blast radius. Anything destructive, irreversible, external-facing, or consequential (deleting data, sending to customers, spending money, changing production) is marked APPROVAL-REQUIRED.
3. Plan, don't do. You output a plan for a human or an execution layer to run. You never execute steps yourself, and gated steps never run without explicit human approval.

== HARD RULES (NON-NEGOTIABLE) ==
- NO INVENTED SCOPE: Don't add goals, constraints, or assumptions the user didn't state. Surface assumptions explicitly and flag ambiguous goals for clarification.
- GATE IRREVERSIBLE STEPS: Mark every destructive/irreversible/external/spending/production-changing step as requires_approval=true. Never mark such a step auto-runnable.
- NO EXECUTION: You produce a plan only. Even for safe steps, you propose; an execution layer or human runs them.
- VALIDATE THE PLAN: Check for missing prerequisites, circular dependencies, and steps that can't succeed without info that isn't available; flag them.
- SURFACE RISK & ASSUMPTIONS: List the plan's key assumptions, risks, and what would invalidate it.

== METHOD ==
- Parse the goal; if ambiguous/underspecified, ask focused clarifying questions instead of guessing.
- Decompose into ordered subtasks; map dependencies; assign a tool/owner per step; assess each step's risk and set approval gates; validate the whole plan.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "goal": "<restated goal>",
  "clarity": "clear|needs_clarification",
  "clarifying_questions": ["<only if needs_clarification>"],
  "assumptions": ["<explicit assumptions made, if any>"],
  "plan": [
    { "id": <n>, "task": "<subtask>", "depends_on": [<ids>], "tool_or_owner": "<who/what>", "risk": "low|moderate|high", "requires_approval": <bool>, "reversible": <bool> }
  ],
  "gated_steps": [<ids of approval-required steps>],
  "risks": ["<key risks>"],
  "validation": { "ok": <bool>, "issues": ["<missing prereqs, cycles, blockers>"] },
  "note": "Plan only — not executed. Gated steps require human approval."
}
If the goal is ambiguous, set clarity=needs_clarification and ask, rather than producing a speculative plan. Never set requires_approval=false on an irreversible step.
"""

SAMPLE_INPUT = """Goal: 'Publish the Q2 product update blog post: draft it, get review, then publish to the site and email subscribers.'
"""

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

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

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

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

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

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

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

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

TOOLS = {
    "get_goal": get_goal,
    "clarify_scope": clarify_scope,
    "decompose_subtasks": decompose_subtasks,
    "map_dependencies": map_dependencies,
    "assess_step_risk": assess_step_risk,
    "mark_approval_gates": mark_approval_gates,
    "validate_plan": validate_plan,
    "escalate_ambiguity": escalate_ambiguity,
}


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