"""
agent.py — a minimal agent in ~60 lines.

An LLM alone is a function: prompt in, text out. One shot. Stateless.
An AGENT is the LOOP that wraps the LLM. It needs:

  1. TOOLS       — functions the LLM is allowed to call
  2. STATE       — conversation history, grows every turn
  3. THE LOOP    — call LLM, run any tool calls it made, feed results
                   back, call LLM again, repeat
  4. TERMINATION — the LLM decides when it's done (you set a safety net)

This file is the smallest useful demo of that pattern using the Claude API.

Run it:
    pip install anthropic
    export ANTHROPIC_API_KEY=sk-...
    python agent.py
"""

import json
import anthropic

client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"  # any tool-capable model


# -----------------------------------------------------------------
# 1. TOOLS — what the agent can do besides talk
# -----------------------------------------------------------------
# Each tool has a JSON schema the LLM sees, plus a Python function
# that runs when the LLM chooses to call it. The schemas are how
# the LLM knows what tools exist and how to call them — treat the
# descriptions like internal docs: clear, specific, no fluff.

TOOLS = [
    {
        "name": "get_weather",
        "description": "Get the weather forecast for a city on a given date.",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string"},
                "date": {"type": "string", "description": "YYYY-MM-DD"},
            },
            "required": ["city", "date"],
        },
    },
]


def run_tool(name: str, args: dict) -> str:
    """The actual implementations. In a real agent these hit APIs,
    databases, the filesystem, other services, etc."""
    if name == "get_weather":
        # Stub — swap in a real API (e.g. Open-Meteo) when you're ready.
        return json.dumps(
            {"high_f": 52, "low_f": 38, "conditions": "rain", "wind_mph": 18}
        )
    raise ValueError(f"Unknown tool: {name}")


# -----------------------------------------------------------------
# 2. THE AGENT LOOP — this is what turns the LLM into an agent
# -----------------------------------------------------------------
def run_agent(user_prompt: str, max_turns: int = 10) -> str:
    # STATE: conversation history. Grows every turn so the LLM can
    # build on what it's already seen (tool results, prior reasoning).
    messages = [{"role": "user", "content": user_prompt}]

    for turn in range(1, max_turns + 1):
        print(f"\n─── Turn {turn} ───")

        response = client.messages.create(
            model=MODEL,
            max_tokens=1024,
            tools=TOOLS,
            messages=messages,
        )

        # Record the assistant's turn in history.
        messages.append({"role": "assistant", "content": response.content})

        # TERMINATION: the agent decides it's done. "end_turn" means
        # the LLM chose to reply with text instead of calling a tool.
        if response.stop_reason == "end_turn":
            final = next(b.text for b in response.content if b.type == "text")
            print(f"\nAGENT: {final}")
            return final

        # TOOL USE: run every tool the LLM called, collect results,
        # feed them back in as the next user turn. Then loop.
        if response.stop_reason == "tool_use":
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    print(f"  CALL   {block.name}({json.dumps(block.input)})")
                    output = run_tool(block.name, block.input)
                    print(f"  RESULT {output}")
                    tool_results.append(
                        {
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": output,
                        }
                    )
            messages.append({"role": "user", "content": tool_results})
            continue

        raise RuntimeError(f"Unexpected stop_reason: {response.stop_reason}")

    raise RuntimeError(f"Agent did not finish within {max_turns} turns")


# -----------------------------------------------------------------
# For comparison: just an LLM — no tools, no loop, no state
# -----------------------------------------------------------------
def run_llm_only(user_prompt: str) -> str:
    response = client.messages.create(
        model=MODEL,
        max_tokens=1024,
        messages=[{"role": "user", "content": user_prompt}],
    )
    text = next(b.text for b in response.content if b.type == "text")
    print(f"LLM: {text}")
    return text


if __name__ == "__main__":
    prompt = "I'm flying to Chicago tomorrow. Should I pack a coat?"

    print("=" * 60)
    print("LLM only  (no tools, no loop, one shot)")
    print("=" * 60)
    run_llm_only(prompt)

    print("\n" + "=" * 60)
    print("Agent     (tools + loop + self-termination)")
    print("=" * 60)
    run_agent(prompt)
