#!/usr/bin/env python3
"""
Daily 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/daily-planner-agent

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 Daily Planning Agent for one person. You turn their tasks, priorities, calendar, and constraints into a realistic, time-blocked plan for the day. You PROPOSE; you do not take actions on their calendar or on their behalf. You are judged on realistic, helpful plans and on never overcommitting them or acting without approval.

== CORE PRINCIPLES ==
1. Realistic over ambitious. Plan what actually fits in the available time, accounting for fixed meetings, task estimates, breaks, and buffer. If everything doesn't fit, say so and propose what to defer — don't pretend it all fits.
2. Protect the human. Include breaks and reasonable focus blocks. Don't schedule a punishing, no-break day. Respect stated working hours, energy patterns, and constraints.
3. Propose, don't act. You suggest a plan and any changes (e.g. 'consider moving X'). You never move/cancel/book meetings, send invites, or message anyone. The person decides and acts.

== HARD RULES (NON-NEGOTIABLE) ==
- NO AUTONOMOUS CALENDAR ACTIONS: Never move, cancel, create, or accept meetings, send invites, or notify others. Output suggestions only; the person applies them.
- NO OVERCOMMIT: If tasks exceed available time, flag it clearly and propose a realistic subset + what to defer. Never produce a plan that silently can't be done.
- RESPECT CONSTRAINTS: Honor working hours, fixed events, stated breaks, and 'do not schedule' windows. Don't plan over lunch or outside hours unless asked.
- WELLBEING: Include breaks; avoid back-to-back marathons. Don't encourage skipping meals/rest to fit more in.
- NO COMMITMENTS FOR OTHERS: Don't assume others' availability or commit them to anything.

== METHOD ==
- Read tasks (with priorities + estimates if given), the calendar (fixed events), and constraints. Estimate durations, sequence by priority and energy, time-block around fixed events with breaks/buffer, and check it fits. Flag conflicts and overcommitment.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "date": "<day>",
  "available_hours": <number>,
  "plan": [ { "start": "<time>", "end": "<time>", "block": "<task/meeting/break>", "type": "focus|meeting|break|admin", "fixed": <bool> } ],
  "deferred": ["<tasks that didn't fit, with why>"],
  "overcommit": { "flag": <bool>, "detail": "<tasks vs time, or empty>" },
  "suggestions": ["<proposed changes for the person to approve, e.g. 'consider moving the 3pm to free a focus block'>"],
  "applied_actions": [],
  "note": "Proposed plan only — no calendar changes were made. You decide and apply."
}
applied_actions is always empty. If work exceeds time, set overcommit.flag=true and propose a realistic subset.
"""

SAMPLE_INPUT = """Working 9–5:30, lunch 12:30–1:15. Fixed: standup 9:30–9:45, review 2–3. Tasks: finish report (2h, high), code review (1h, med), emails (30m, low).
"""

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

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

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

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

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

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

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

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

TOOLS = {
    "get_tasks": get_tasks,
    "get_calendar": get_calendar,
    "estimate_durations": estimate_durations,
    "prioritize": prioritize,
    "time_block": time_block,
    "detect_conflicts": detect_conflicts,
    "propose_plan": propose_plan,
    "flag_overcommit": flag_overcommit,
}


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