{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Module 4 — Scaling with Message Batches\n",
    "\n",
    "The Message Batches API runs high-volume, non-time-sensitive work asynchronously at **50% off** standard pricing. Most batches finish in under an hour; the API guarantees completion within 24 hours.\n",
    "\n",
    "> **ZDR warning:** Batches are **not ZDR-eligible**. Inputs and results are stored server-side for up to 29 days. Don't batch PHI when ZDR is required."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Setup"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import anthropic\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "load_dotenv()\n",
    "client = anthropic.Anthropic()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Submit a batch\n",
    "\n",
    "Each request needs a unique `custom_id` — that's the only way to map results back to inputs, since batch results don't return in submission order. The first request also enables the 300k-token output beta header."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from anthropic.types.message_create_params import MessageCreateParamsNonStreaming\n",
    "from anthropic.types.messages.batch_create_params import Request\n",
    "\n",
    "message_batch = client.messages.batches.create(\n",
    "    requests=[\n",
    "        Request(\n",
    "            custom_id=\"prospect-fintech-001\",\n",
    "            params=MessageCreateParamsNonStreaming(\n",
    "                model=\"claude-sonnet-4-6\",\n",
    "                max_tokens=300000,\n",
    "                extra_headers={\"anthropic-beta\": \"output-300k-2026-03-24\"},\n",
    "                messages=[{\"role\": \"user\", \"content\": \"Write a 200-page AI consulting guide for Fintech CTOs.\"}],\n",
    "            ),\n",
    "        ),\n",
    "        Request(\n",
    "            custom_id=\"prospect-healthcare-002\",\n",
    "            params=MessageCreateParamsNonStreaming(\n",
    "                model=\"claude-sonnet-4-6\",\n",
    "                max_tokens=1024,\n",
    "                messages=[{\"role\": \"user\", \"content\": \"Write a personalized outreach email for a Healthcare CEO.\"}],\n",
    "            ),\n",
    "        ),\n",
    "    ],\n",
    ")\n",
    "\n",
    "print(\"batch id:\", message_batch.id)  # always begins with msgbatch_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Poll for completion\n",
    "\n",
    "`processing_status` flips from `\"in_progress\"` to `\"ended\"` once every request has finished (succeeded, errored, expired, or canceled)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "\n",
    "BATCH_ID = message_batch.id\n",
    "\n",
    "while True:\n",
    "    status_update = client.messages.batches.retrieve(BATCH_ID)\n",
    "    if status_update.processing_status == \"ended\":\n",
    "        print(\"Batch processing complete!\")\n",
    "        break\n",
    "    counts = status_update.request_counts\n",
    "    print(f\"Still processing... (Succeeded: {counts.succeeded}, Errored: {counts.errored})\")\n",
    "    time.sleep(60)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Stream and map results\n",
    "\n",
    "Always key on `custom_id`. You're only billed for `succeeded` results — `errored`, `expired`, and `canceled` requests are free."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for result in client.messages.batches.results(BATCH_ID):\n",
    "    request_id = result.custom_id\n",
    "\n",
    "    if result.result.type == \"succeeded\":\n",
    "        content = result.result.message.content[0].text\n",
    "        print(f\"Success for {request_id}: {content[:50]}...\")\n",
    "\n",
    "    elif result.result.type == \"errored\":\n",
    "        error_type = result.result.error.error.type\n",
    "        print(f\"Error for {request_id}: {error_type}\")\n",
    "\n",
    "    elif result.result.type == \"expired\":\n",
    "        print(f\"Request {request_id} timed out (24-hour limit reached).\")\n",
    "\n",
    "    elif result.result.type == \"canceled\":\n",
    "        print(f\"Request {request_id} was canceled before completion.\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
