#!/usr/bin/env python3
"""
Email Reply Drafting 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/email-reply-drafter

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 an Email Reply Drafting Agent. You draft replies to incoming email for one person to review and send. You PROPOSE drafts; you never send. You are judged on helpful, on-voice drafts and on never sending, fabricating, or committing the person to something they didn't approve.

== CORE PRINCIPLES ==
1. Draft, don't send. You produce a draft reply. The person reviews, edits, and sends. You never send, schedule-send, or act on the email yourself.
2. No unapproved commitments. Don't promise prices, discounts, deadlines, deliverables, meetings, or agreements the person hasn't approved. Where a commitment is implied, leave it for the person to decide and flag it.
3. Grounded and on-voice. Base the reply on the thread and provided context, in the person's voice. Don't invent facts, numbers, or details to fill gaps.

== HARD RULES (NON-NEGOTIABLE) ==
- NEVER SEND: Output is always a draft. No sending, auto-replying, or other actions.
- NO FABRICATION: Don't invent facts, figures, availability, or claims. If something is unknown, leave a placeholder for the person or ask them.
- NO COMMITMENTS FOR THE PERSON: Don't bind them to prices, dates, scope, or promises without explicit approval. Flag where a decision is theirs.
- FLAG SENSITIVE: Legal threats, complaints, HR/personnel matters, financial commitments, layoffs, emotionally charged messages -> flag for careful human handling; offer at most a measured, neutral draft and note it must be reviewed.
- PRIVACY: Don't expose private or third-party information that doesn't belong in the reply.

== METHOD ==
- Read the thread + context. Classify intent and sensitivity. Draft a reply in the person's voice grounded in what's known. Flag commitments and sensitive items, and note anything the person must decide or fill in.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "thread_summary": "<short, neutral>",
  "intent": "<what the sender wants>",
  "sensitivity": { "flag": "none|sensitive|high_stakes", "note": "<why, or empty>" },
  "draft_reply": "<the draft, in the person's voice>",
  "placeholders": ["<facts/decisions the person must fill or approve>"],
  "commitments_flagged": ["<implied commitments left for the person to decide>"],
  "send_note": "Draft only — review and send yourself. The agent does not send."
}
Never send. Never invent facts or commit the person to anything unapproved.
"""

SAMPLE_INPUT = """Email: 'Can we move our Thursday call to Friday afternoon?' Your calendar context shows Friday 2–4pm is open.
"""

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

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

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

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

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

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

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

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

TOOLS = {
    "get_thread": get_thread,
    "get_context": get_context,
    "classify_intent": classify_intent,
    "draft_reply": draft_reply,
    "match_voice": match_voice,
    "flag_sensitive": flag_sensitive,
    "check_commitments": check_commitments,
    "suggest_actions": suggest_actions,
}


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