{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*Lab 1 Self-Driven Lab*\n",
    "\n",
    "# Claude API Mechanics & Integration\n",
    "\n",
    "**Objective:** build a small client module that survives the real-world failure modes of the Messages API, then prove each one.\n",
    "\n",
    "## Challenge Outline\n",
    "\n",
    "Build a complete notebook that demonstrates the following outcomes:\n",
    "\n",
    "- **Block-safe extraction:** write `extract_text(response)` that walks `response.content` and concatenates only `text` blocks, then prove it survives adaptive thinking where `content[0].text` would crash.\n",
    "- **Stop-reason dispatch:** write a handler that branches on all six stop reasons, then force `max_tokens` truncation with `max_tokens=16` and confirm your code detects it rather than treating it as a complete answer.\n",
    "- **Prove the 400s:** send one request with `budget_tokens` and one with `temperature`, capture the `BadRequestError` for each, and read the message so you never write those shapes again.\n",
    "- **Stream with a final message:** stream a long generation, print deltas as they arrive, then read `stop_reason` and `usage` off `get_final_message()`.\n",
    "- **Typed error chain:** trigger a `NotFoundError` with a bogus model ID, confirm your handler does not retry it, then set `max_retries=0` and observe the difference on retryable errors.\n",
    "- **Batch, keyed correctly:** submit 20 short requests as a batch with meaningful `custom_id`s, deliberately reassemble results positionally to observe the mismatch, then fix it by keying on `custom_id`.\n",
    "- **Multi-format input:** send an image and a PDF in one user turn and ask a question that requires both.\n",
    "\n",
    "Your solution should include enough code, output, or written observations to prove each outcome worked. Keep the notebook focused on final behavior and evidence rather than a guided walkthrough.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Student Workspace\n",
    "\n",
    "Use the sections below to build your solution. Each section maps to one required outcome from the challenge outline.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Part 1: Block-safe extraction\n",
    "\n",
    "write `extract_text(response)` that walks `response.content` and concatenates only `text` blocks, then prove it survives adaptive thinking where `content[0].text` would crash.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Part 1: Block-safe extraction\n",
    "# Add your implementation, outputs, or notes here.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Part 2: Stop-reason dispatch\n",
    "\n",
    "write a handler that branches on all six stop reasons, then force `max_tokens` truncation with `max_tokens=16` and confirm your code detects it rather than treating it as a complete answer.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Part 2: Stop-reason dispatch\n",
    "# Add your implementation, outputs, or notes here.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Part 3: Prove the 400s\n",
    "\n",
    "send one request with `budget_tokens` and one with `temperature`, capture the `BadRequestError` for each, and read the message so you never write those shapes again.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Part 3: Prove the 400s\n",
    "# Add your implementation, outputs, or notes here.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Part 4: Stream with a final message\n",
    "\n",
    "stream a long generation, print deltas as they arrive, then read `stop_reason` and `usage` off `get_final_message()`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Part 4: Stream with a final message\n",
    "# Add your implementation, outputs, or notes here.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Part 5: Typed error chain\n",
    "\n",
    "trigger a `NotFoundError` with a bogus model ID, confirm your handler does not retry it, then set `max_retries=0` and observe the difference on retryable errors.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Part 5: Typed error chain\n",
    "# Add your implementation, outputs, or notes here.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Part 6: Batch, keyed correctly\n",
    "\n",
    "submit 20 short requests as a batch with meaningful `custom_id`s, deliberately reassemble results positionally to observe the mismatch, then fix it by keying on `custom_id`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Part 6: Batch, keyed correctly\n",
    "# Add your implementation, outputs, or notes here.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Part 7: Multi-format input\n",
    "\n",
    "send an image and a PDF in one user turn and ask a question that requires both.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Part 7: Multi-format input\n",
    "# Add your implementation, outputs, or notes here.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Verification Notes\n",
    "\n",
    "Summarize the evidence that each part worked. Capture API signals, validation outcomes, errors, recovery behavior, cost observations, or comparisons required by this lab.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Verification notes\n",
    "# Record the evidence that proves each lab outcome worked.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## Answer Key\n",
    "\n",
    "The cells below contain the completed reference implementation/content for this lab. Use this section only after attempting the self-driven lab.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*Lab 1 Answer Key*\n",
    "\n",
    "# Claude API Mechanics & Integration\n",
    "\n",
    "**Objective:** build a small client module that survives the real-world failure modes of the Messages API, then prove each one.\n",
    "\n",
    "Everything is one endpoint: `POST /v1/messages`. Tools, structured output, thinking, vision, and caching are parameters on that one call, not separate APIs. This lab builds a resilient client around the four things that break real integrations: typed content blocks, stop reasons, error handling, and the realtime-versus-batch decision.\n",
    "\n",
    "**Setup:** `pip install anthropic` and set `ANTHROPIC_API_KEY` in your environment (or use an `ant auth login` profile). Never hardcode a key.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 1: Block-safe extraction\n",
    "\n",
    "The single most common integration bug is `response.content[0].text`. It works until the day you enable thinking or tools, and then block zero is a `thinking` or `tool_use` block and you get an `AttributeError` in production. Always narrow by `.type`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import anthropic\n",
    "\n",
    "client = anthropic.Anthropic()  # resolves credentials from the environment\n",
    "\n",
    "\n",
    "def extract_text(response):\n",
    "    \"\"\"Concatenate only text blocks. Safe under thinking and tool use.\"\"\"\n",
    "    return \"\".join(b.text for b in response.content if b.type == \"text\")\n",
    "\n",
    "\n",
    "# Prove it: with adaptive thinking on, a thinking block precedes the text,\n",
    "# so content[0].text would raise AttributeError. extract_text does not care.\n",
    "response = client.messages.create(\n",
    "    model=\"claude-opus-4-8\",\n",
    "    max_tokens=16000,\n",
    "    thinking={\"type\": \"adaptive\"},\n",
    "    messages=[{\"role\": \"user\", \"content\": \"In one sentence: why do ships float?\"}],\n",
    ")\n",
    "\n",
    "print(\"block types:\", [b.type for b in response.content])\n",
    "print(\"text:\", extract_text(response))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 2: Stop-reason dispatch\n",
    "\n",
    "Never parse the text to decide what to do next; branch on `stop_reason`. Guard `stop_details` before reading it: it is populated only when `stop_reason == \"refusal\"` and is `None` for every other stop reason.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def dispatch(response):\n",
    "    \"\"\"Map every stop_reason to the action the caller must take.\"\"\"\n",
    "    reason = response.stop_reason\n",
    "    if reason == \"end_turn\":\n",
    "        return (\"done\", extract_text(response))\n",
    "    if reason == \"tool_use\":\n",
    "        return (\"run_tools\", [b for b in response.content if b.type == \"tool_use\"])\n",
    "    if reason == \"max_tokens\":\n",
    "        return (\"truncated\", \"raise max_tokens or stream; output ended mid-thought\")\n",
    "    if reason == \"stop_sequence\":\n",
    "        return (\"stop_sequence\", response.stop_sequence)\n",
    "    if reason == \"pause_turn\":\n",
    "        return (\"resume\", \"re-send the conversation as-is; do NOT append 'Continue.'\")\n",
    "    if reason == \"refusal\":\n",
    "        # stop_details is populated ONLY here; None everywhere else.\n",
    "        detail = response.stop_details.category if response.stop_details else None\n",
    "        return (\"refused\", detail)\n",
    "    return (\"unknown\", reason)\n",
    "\n",
    "\n",
    "# Force truncation: a long request against a 16-token cap.\n",
    "short = client.messages.create(\n",
    "    model=\"claude-opus-4-8\",\n",
    "    max_tokens=16,\n",
    "    messages=[{\"role\": \"user\", \"content\": \"Explain TCP congestion control in detail.\"}],\n",
    ")\n",
    "action, payload = dispatch(short)\n",
    "assert action == \"truncated\", f\"expected truncation, got {action}\"\n",
    "print(\"detected:\", action, \"-\", payload)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 3: Prove the 400s\n",
    "\n",
    "These shapes are removed on current models and return HTTP 400, not a deprecation warning. The point of this part is to see the rejection with your own eyes. The requests below are deliberate counter-examples: they are supposed to fail.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# DELIBERATE COUNTER-EXAMPLES: both requests below fail with HTTP 400.\n",
    "# budget_tokens is removed; use thinking={\"type\": \"adaptive\"} instead.\n",
    "try:\n",
    "    client.messages.create(\n",
    "        model=\"claude-opus-4-8\",\n",
    "        max_tokens=16000,\n",
    "        thinking={\"type\": \"enabled\", \"budget_tokens\": 8000},  # removed -> 400\n",
    "        messages=[{\"role\": \"user\", \"content\": \"hi\"}],\n",
    "    )\n",
    "except anthropic.BadRequestError as e:\n",
    "    print(\"budget_tokens rejected:\", e.message)\n",
    "\n",
    "# temperature (and top_p / top_k) are removed on current models -> 400.\n",
    "try:\n",
    "    client.messages.create(\n",
    "        model=\"claude-opus-4-8\",\n",
    "        max_tokens=16000,\n",
    "        temperature=0.7,  # removed -> 400\n",
    "        messages=[{\"role\": \"user\", \"content\": \"hi\"}],\n",
    "    )\n",
    "except anthropic.BadRequestError as e:\n",
    "    print(\"temperature rejected:\", e.message)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The current shapes are `thinking={\"type\": \"adaptive\"}` for reasoning and `output_config={\"effort\": \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\"}` for depth. Steer style with prompting, not sampling parameters.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 4: Stream with a final message\n",
    "\n",
    "Stream whenever output could be long. The SDK refuses non-streaming requests it estimates will exceed the HTTP timeout, so large `max_tokens` requires streaming. The `stream()` helper accumulates state for you; `get_final_message()` returns the complete `Message` with `stop_reason` and `usage`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "with client.messages.stream(\n",
    "    model=\"claude-opus-4-8\",\n",
    "    max_tokens=64000,\n",
    "    messages=[{\"role\": \"user\", \"content\": \"Write a detailed migration plan for moving a monolith to services.\"}],\n",
    ") as stream:\n",
    "    for text in stream.text_stream:\n",
    "        print(text, end=\"\", flush=True)\n",
    "\n",
    "    final = stream.get_final_message()\n",
    "\n",
    "print(\"\\n\\nstop:\", final.stop_reason, \"| out tokens:\", final.usage.output_tokens)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 5: Typed error chain\n",
    "\n",
    "Catch a chain, most specific first. 400/401/404 are fatal and must never be retried; 429, 5xx, and network failures are retryable. The SDK already retries the retryable ones (default `max_retries=2`), so custom retry logic is only for behavior the SDK does not provide.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def call_safely(client, **kwargs):\n",
    "    \"\"\"Typed error chain: fatal errors raise, retryable errors report.\"\"\"\n",
    "    try:\n",
    "        return client.messages.create(**kwargs)\n",
    "    except anthropic.NotFoundError:\n",
    "        print(\"404: bad model ID or endpoint. NEVER retry.\")\n",
    "        raise\n",
    "    except anthropic.BadRequestError as e:\n",
    "        print(\"400: fix the request, do not retry:\", e.message)\n",
    "        raise\n",
    "    except anthropic.AuthenticationError:\n",
    "        print(\"401: bad or missing key. NEVER retry.\")\n",
    "        raise\n",
    "    except anthropic.RateLimitError as e:\n",
    "        wait = int(e.response.headers.get(\"retry-after\", \"60\"))\n",
    "        print(f\"429: retryable, back off {wait}s\")\n",
    "        raise\n",
    "    except anthropic.APIStatusError as e:\n",
    "        print(\"retryable\" if e.status_code >= 500 else \"fatal\", e.status_code)\n",
    "        raise\n",
    "    except anthropic.APIConnectionError:\n",
    "        print(\"network failure before any response: retryable\")\n",
    "        raise\n",
    "\n",
    "\n",
    "try:\n",
    "    call_safely(\n",
    "        client,\n",
    "        model=\"claude-opus-9-9\",  # bogus on purpose\n",
    "        max_tokens=64,\n",
    "        messages=[{\"role\": \"user\", \"content\": \"hi\"}],\n",
    "    )\n",
    "except anthropic.NotFoundError:\n",
    "    print(\"confirmed: handler surfaced the 404 without retrying\")\n",
    "\n",
    "# The SDK retries 429/5xx/network twice by default. Disable to compare:\n",
    "no_retry_client = anthropic.Anthropic(max_retries=0)\n",
    "print(\"default retries:\", client.max_retries, \"| disabled:\", no_retry_client.max_retries)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 6: Batch, keyed correctly\n",
    "\n",
    "The Message Batches API processes latency-tolerant workloads within a 24-hour window at 50% of standard token cost. Results arrive in ANY order: keying by position is a silent data-corruption bug, because every summary attaches to the wrong document and nothing throws.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "from anthropic.types.message_create_params import MessageCreateParamsNonStreaming\n",
    "from anthropic.types.messages.batch_create_params import Request\n",
    "\n",
    "documents = [f\"Document {i}: the quarterly total for region {i} is {i * 111}.\" for i in range(20)]\n",
    "\n",
    "batch = client.messages.batches.create(requests=[\n",
    "    Request(\n",
    "        custom_id=f\"doc-{i}\",  # YOUR correlation key\n",
    "        params=MessageCreateParamsNonStreaming(\n",
    "            model=\"claude-haiku-4-5\",\n",
    "            max_tokens=64,\n",
    "            messages=[{\"role\": \"user\", \"content\": f\"Repeat only the number in: {doc}\"}],\n",
    "        ),\n",
    "    )\n",
    "    for i, doc in enumerate(documents)\n",
    "])\n",
    "\n",
    "while client.messages.batches.retrieve(batch.id).processing_status != \"ended\":\n",
    "    time.sleep(30)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# WRONG: positional reassembly. Results are unordered, so this maps\n",
    "# answers to the wrong documents and never raises.\n",
    "raw = list(client.messages.batches.results(batch.id))\n",
    "positional = {documents[i]: r for i, r in enumerate(raw)}\n",
    "mismatches = sum(\n",
    "    1 for i, r in enumerate(raw) if r.custom_id != f\"doc-{i}\"\n",
    ")\n",
    "print(\"results whose position does not match their custom_id:\", mismatches)\n",
    "\n",
    "# RIGHT: key by custom_id.\n",
    "results = {}\n",
    "for result in client.messages.batches.results(batch.id):\n",
    "    if result.result.type == \"succeeded\":\n",
    "        msg = result.result.message\n",
    "        results[result.custom_id] = next(\n",
    "            (b.text for b in msg.content if b.type == \"text\"), \"\"\n",
    "        )\n",
    "    elif result.result.type == \"errored\":\n",
    "        # \"invalid_request\" = fix and resubmit. Anything else = safe to retry.\n",
    "        print(\"errored:\", result.custom_id, result.result.error)\n",
    "\n",
    "for i in range(3):\n",
    "    print(f\"doc-{i} ->\", results.get(f\"doc-{i}\"))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 7: Multi-format input\n",
    "\n",
    "Images and documents are content blocks inside the user turn, not a different endpoint. Put the document block before the text block that refers to it. For files reused across many requests, upload once via the Files API and reference by `file_id` instead of re-encoding megabytes on every call.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import base64\n",
    "\n",
    "with open(\"chart.png\", \"rb\") as f:\n",
    "    img = base64.standard_b64encode(f.read()).decode(\"utf-8\")\n",
    "\n",
    "with open(\"report.pdf\", \"rb\") as f:\n",
    "    pdf = base64.standard_b64encode(f.read()).decode(\"utf-8\")\n",
    "\n",
    "response = client.messages.create(\n",
    "    model=\"claude-opus-4-8\",\n",
    "    max_tokens=16000,\n",
    "    messages=[{\n",
    "        \"role\": \"user\",\n",
    "        \"content\": [\n",
    "            {\"type\": \"image\",\n",
    "             \"source\": {\"type\": \"base64\", \"media_type\": \"image/png\", \"data\": img}},\n",
    "            # Document block goes BEFORE the text block it refers to.\n",
    "            {\"type\": \"document\",\n",
    "             \"source\": {\"type\": \"base64\", \"media_type\": \"application/pdf\", \"data\": pdf}},\n",
    "            {\"type\": \"text\", \"text\": \"Does the chart agree with the report's Q3 figures?\"},\n",
    "        ],\n",
    "    }],\n",
    ")\n",
    "print(extract_text(response))\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
