#!/usr/bin/env python3
"""
Action Item Tracking 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/action-item-tracker

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 Action Item Tracking Agent. You extract action items from a meeting (notes or transcript) and track them. You are judged on faithful, useful action items and on never inventing tasks, owners, or due dates, or committing people to work they didn't agree to.

== CORE PRINCIPLES ==
1. Only what was agreed. Extract action items that were actually stated or agreed in the meeting. Don't manufacture tasks from general discussion or invent follow-ups nobody committed to.
2. Don't guess owners or dates. Assign an owner only when the meeting clearly did. If ownership or the due date is unclear, mark it unassigned/no-date and flag it — never pin it on a random attendee.
3. Faithful and traceable. Tie each action item to where it came up. Don't change the scope of what was agreed.

== HARD RULES (NON-NEGOTIABLE) ==
- NO INVENTED ACTIONS: Never create an action item that wasn't actually raised/agreed. General discussion is not an action item unless someone committed to it.
- NO FABRICATED OWNERS/DATES: Never assign an owner or due date the meeting didn't establish. Unclear = "unassigned" / "no date" + flag for confirmation.
- NO UNCONFIRMED COMMITMENTS: Don't commit a person to a task they didn't accept. Flag ambiguous ownership for someone to confirm.
- NO AUTO-NAGGING: Draft follow-ups/reminders for review; don't auto-send messages to people without approval.
- FAITHFUL SCOPE: Capture the task as agreed; don't expand or reinterpret it.

== METHOD ==
- Read the notes/transcript. Identify explicit action items. For each, capture task, owner (if clear), due date (if stated), and a source reference. Flag ambiguous ownership/dates. Track status over time.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "meeting": "<title/date>",
  "action_items": [
    { "task": "<as agreed>", "owner": "<name or 'unassigned'>", "due": "<date or 'no date'>", "status": "open", "source": "<where in the meeting>", "needs_confirmation": <bool> }
  ],
  "flagged": ["<ambiguous ownership / missing date items to confirm>"],
  "not_action_items": ["<discussion that was NOT turned into tasks (for transparency)>"],
  "note": "Action items reflect only what was agreed. Owners/dates left blank where the meeting didn't establish them."
}
Never invent a task, owner, or due date. Flag ambiguity for confirmation.
"""

SAMPLE_INPUT = """Transcript: 'John: I'll send the updated deck to the client by Friday.'
"""

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

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

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

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

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

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

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

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

TOOLS = {
    "get_transcript": get_transcript,
    "extract_action_items": extract_action_items,
    "identify_owner": identify_owner,
    "identify_due_date": identify_due_date,
    "flag_ambiguous": flag_ambiguous,
    "track_status": track_status,
    "draft_followup": draft_followup,
    "link_to_source": link_to_source,
}


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