{
 "nbformat": 4,
 "nbformat_minor": 5,
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "provenance": []
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 🏛️ Hands-on: the Claude API, by actually using it\n",
    "\n",
    "**Colab edition** of the [10 progressive exercises](https://github.com/) from the *Claude Certified Architect*\n",
    "prep track. Each exercise has a **goal**, runnable **code**, what to **observe**, and the **learning**.\n",
    "Run them **in order** — each builds on the last.\n",
    "\n",
    "> 💰 **Cost:** everything here uses Haiku or short calls — the whole notebook costs a **few US cents**.\n",
    "> 🔑 **Key:** you need an Anthropic API key ([how to get one](https://console.anthropic.com) — see the track's\n",
    "> *Getting your API key* guide for billing setup and safety rules). The next cell asks for it with a **hidden\n",
    "> prompt** — it is stored only in this runtime's memory, never in the notebook file.\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Setup — run me first\n",
    "%pip install -q anthropic\n",
    "\n",
    "import os, getpass\n",
    "if not os.environ.get(\"ANTHROPIC_API_KEY\"):\n",
    "    os.environ[\"ANTHROPIC_API_KEY\"] = getpass.getpass(\"Paste your Anthropic API key (input is hidden): \")\n",
    "\n",
    "import anthropic\n",
    "client = anthropic.Anthropic()\n",
    "print(\"Client ready ✓\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 1 — Your first call (prove the doorway works)\n",
    "**Goal:** send one message, get one reply.\n",
    "\n",
    "**Observe:** a one-sentence answer prints. **Learning:** this is the whole API — one call, `content[0].text`\n",
    "is the reply. A 401 error means your key isn't set correctly (re-run the setup cell)."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "msg = client.messages.create(\n",
    "    model=\"claude-haiku-4-5\",\n",
    "    max_tokens=100,\n",
    "    messages=[{\"role\": \"user\", \"content\": \"In one sentence, what is an API?\"}],\n",
    ")\n",
    "print(msg.content[0].text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 2 — Inspect the whole response object\n",
    "**Goal:** see what *else* comes back besides the text.\n",
    "\n",
    "**Observe:** `stop_reason` = `end_turn`; `usage` shows token counts (your bill). **Learning:** the reply is an\n",
    "*object*, not just a string. You'll check `stop_reason` and `usage` constantly."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "msg = client.messages.create(\n",
    "    model=\"claude-haiku-4-5\", max_tokens=100,\n",
    "    messages=[{\"role\": \"user\", \"content\": \"Name three primary colors.\"}],\n",
    ")\n",
    "print(\"text:       \", msg.content[0].text)\n",
    "print(\"stop_reason:\", msg.stop_reason)          # why it stopped\n",
    "print(\"usage:      \", msg.usage)                # input/output tokens (your bill)\n",
    "print(\"model:      \", msg.model)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 3 — Feel `max_tokens` cut a reply off\n",
    "**Goal:** trigger a truncated response on purpose.\n",
    "\n",
    "**Observe:** at 20 the text is cut mid-thought and `stop_reason == \"max_tokens\"`; at 500 it finishes with\n",
    "`end_turn`. **Learning:** `max_tokens` caps the *reply*. Truncated output? Check `stop_reason`, raise the cap —\n",
    "a real debugging reflex."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "for cap in (20, 500):\n",
    "    msg = client.messages.create(\n",
    "        model=\"claude-haiku-4-5\", max_tokens=cap,\n",
    "        messages=[{\"role\": \"user\", \"content\": \"Explain how rainbows form.\"}],\n",
    "    )\n",
    "    print(f\"\\n--- max_tokens={cap} | stop_reason={msg.stop_reason} ---\")\n",
    "    print(msg.content[0].text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 4 — Temperature: repeatable vs varied\n",
    "**Goal:** see determinism appear at `temperature=0`.\n",
    "\n",
    "**Observe:** high temp → variety; temp 0 → (near-)identical each run. **Learning:** set `temperature=0`\n",
    "whenever you need consistency (extraction, classification, evals). Leaving it high is why \"the same prompt\n",
    "gives different answers\"."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "def ask(temp):\n",
    "    m = client.messages.create(\n",
    "        model=\"claude-haiku-4-5\", max_tokens=30, temperature=temp,\n",
    "        messages=[{\"role\": \"user\", \"content\": \"Invent a name for a coffee shop.\"}],\n",
    "    )\n",
    "    return m.content[0].text.strip()\n",
    "\n",
    "print(\"temp=1.0:\", [ask(1.0) for _ in range(3)])   # three different names\n",
    "print(\"temp=0.0:\", [ask(0.0) for _ in range(3)])   # nearly identical"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 5 — The `system` prompt changes behavior\n",
    "**Goal:** the same question answered in two personas.\n",
    "\n",
    "**Observe:** wildly different tone/level, same question. **Learning:** `system` is a *separate parameter*\n",
    "(not a message) that sets role + rules — your main lever for controlling behavior."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "def ask(system):\n",
    "    m = client.messages.create(\n",
    "        model=\"claude-haiku-4-5\", max_tokens=120, system=system,\n",
    "        messages=[{\"role\": \"user\", \"content\": \"What is a black hole?\"}],\n",
    "    )\n",
    "    return m.content[0].text\n",
    "\n",
    "print(ask(\"You explain things to a curious 5-year-old, with a simple analogy.\"))\n",
    "print(\"\\n---\\n\")\n",
    "print(ask(\"You are an astrophysicist writing for a graduate seminar.\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 6 — Multi-turn: prove statelessness yourself\n",
    "**Goal:** show Claude \"forgets\" unless you resend history.\n",
    "\n",
    "**Observe:** the first path can't recall the name; the second can. **Learning:** the API is **stateless** —\n",
    "*you* own the conversation and resend it every turn. This is *why* long chats fill the context window."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# WRONG way — no history carried:\n",
    "client.messages.create(model=\"claude-haiku-4-5\", max_tokens=50,\n",
    "    messages=[{\"role\": \"user\", \"content\": \"My cat is named Pip.\"}])\n",
    "r = client.messages.create(model=\"claude-haiku-4-5\", max_tokens=50,\n",
    "    messages=[{\"role\": \"user\", \"content\": \"What is my cat's name?\"}])\n",
    "print(\"no-history →\", r.content[0].text)      # it does NOT know\n",
    "\n",
    "# RIGHT way — carry the whole conversation:\n",
    "history = [\n",
    "    {\"role\": \"user\", \"content\": \"My cat is named Pip.\"},\n",
    "    {\"role\": \"assistant\", \"content\": \"Nice to meet Pip!\"},\n",
    "    {\"role\": \"user\", \"content\": \"What is my cat's name?\"},\n",
    "]\n",
    "r = client.messages.create(model=\"claude-haiku-4-5\", max_tokens=50, messages=history)\n",
    "print(\"with-history →\", r.content[0].text)     # → \"Pip\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 7 — Build a tiny chatbot\n",
    "**Goal:** turn statelessness into a working chat loop (3 turns here — extend it if you like).\n",
    "\n",
    "**Observe:** it remembers across turns because *you* keep appending. **Learning:** a chatbot is just:\n",
    "append user turn → call → append assistant turn → repeat. That append **is** the entire \"memory\" —\n",
    "exactly what claude.ai does for you invisibly."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "history = []\n",
    "for turn in range(3):                       # 3 turns; change to while True for endless chat\n",
    "    user = input(f\"you ({turn+1}/3): \")\n",
    "    history.append({\"role\": \"user\", \"content\": user})\n",
    "    msg = client.messages.create(\n",
    "        model=\"claude-haiku-4-5\", max_tokens=300,\n",
    "        system=\"You are a concise, friendly assistant.\",\n",
    "        messages=history,\n",
    "    )\n",
    "    reply = msg.content[0].text\n",
    "    print(\"claude:\", reply, \"\\n\")\n",
    "    history.append({\"role\": \"assistant\", \"content\": reply})   # ← append the reply!\n",
    "\n",
    "print(f\"history now holds {len(history)} messages — this is what gets resent every turn.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 8 — Stream the reply (typing effect)\n",
    "**Goal:** see tokens arrive live.\n",
    "\n",
    "**Observe:** text appears word-by-word instead of all at once. **Learning:** use `stream()` for chat UIs;\n",
    "plain `create()` for backend/batch. Same request, different delivery."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "with client.messages.stream(\n",
    "    model=\"claude-haiku-4-5\", max_tokens=200,\n",
    "    messages=[{\"role\": \"user\", \"content\": \"Write a haiku about the ocean.\"}],\n",
    ") as stream:\n",
    "    for text in stream.text_stream:\n",
    "        print(text, end=\"\", flush=True)\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 9 — Handle errors like a grown-up\n",
    "**Goal:** catch and read a real API error (a deliberately wrong model name).\n",
    "\n",
    "**Observe:** a clean, catchable exception instead of a crash. **Learning:** the SDK raises typed errors\n",
    "(`APIStatusError`, `RateLimitError`, `AuthenticationError`). 401 = key problem, 429 = rate limit,\n",
    "400 = bad request, 404 = wrong model name."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "try:\n",
    "    client.messages.create(\n",
    "        model=\"claude-does-not-exist\", max_tokens=10,\n",
    "        messages=[{\"role\": \"user\", \"content\": \"hi\"}],\n",
    "    )\n",
    "except anthropic.APIStatusError as e:\n",
    "    print(\"status:\", e.status_code)   # 404 / 400\n",
    "    print(\"body:  \", e.message)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 10 — Mini use-case: a support-ticket classifier\n",
    "**Goal:** combine everything into something real.\n",
    "\n",
    "**Learning:** you just built a real classifier — Haiku (cheap, high-volume), `temperature=0` (repeatable),\n",
    "`system` for the rules, small `max_tokens`. Every choice is an architect decision you can now *defend*."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "TICKETS = [\n",
    "    \"I was charged twice this month!\",\n",
    "    \"The app crashes when I open settings.\",\n",
    "    \"Please cancel my subscription.\",\n",
    "]\n",
    "\n",
    "def classify(ticket):\n",
    "    m = client.messages.create(\n",
    "        model=\"claude-haiku-4-5\", max_tokens=20, temperature=0,   # temp 0 = consistent\n",
    "        system=\"Classify the support ticket into exactly one of: billing, technical, cancellation. \"\n",
    "               \"Reply with ONLY that one word, lowercase.\",\n",
    "        messages=[{\"role\": \"user\", \"content\": ticket}],\n",
    "    )\n",
    "    return m.content[0].text.strip()\n",
    "\n",
    "for t in TICKETS:\n",
    "    print(f\"{classify(t):12}  <- {t}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "## ✅ Checkpoint — you've \"got it\" when you can, without looking:\n",
    "\n",
    "- [ ] Explain the difference between the **API**, the **SDK**, and **Claude Code**.\n",
    "- [ ] Write a `messages.create` call from memory with model, max_tokens, system, messages.\n",
    "- [ ] Say what `content[0].text`, `stop_reason`, and `usage` are.\n",
    "- [ ] Explain why the chatbot needs you to **append** the assistant reply.\n",
    "- [ ] Say when you'd set `temperature=0` and why.\n",
    "- [ ] Predict what `stop_reason` you'll get if `max_tokens` is too small.\n",
    "\n",
    "Miss any? Re-run that exercise. **Running it beats re-reading it.**\n",
    "\n",
    "**Next on the track:** the *Prompt Engineering* domain — where Exercise 10's classifier grows a JSON schema,\n",
    "few-shot examples, and an eval. Head back to the learning site → `illustrated/4-prompting/prompt-anatomy.html`.\n"
   ]
  }
 ]
}