{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Module 2 — Tool Design & Schema Safety\n",
    "\n",
    "A multi-stage walkthrough of how Claude selects tools, how the agentic loop splits work between server-side and client-side execution, and how strict schemas prevent runtime errors. By the end of this notebook you will:\n",
    "\n",
    "1. Understand how tool **descriptions** drive selection.\n",
    "2. Define a toolbox that mixes server-side built-ins (`web_search_20260209`, `code_execution_20260120`) with a client-side custom tool (`add_lead_to_crm`).\n",
    "3. Inspect `stop_reason` to drive the agentic loop.\n",
    "4. Handle the loop's client-side leg (CRM write).\n",
    "5. Verify grounding with citations on the final answer."
   ]
  },
  {
   "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. Concepts: Tool Selection & Strict Mode\n",
    "\n",
    "**Tool descriptions are the primary selection mechanism.** Claude reads each tool's `description` and `name` to decide which tool to call. If two tools have overlapping purposes (e.g. `analyze_content` vs. `analyze_document`), Claude may misroute the request — write descriptions that disambiguate exactly when each tool is appropriate.\n",
    "\n",
    "**Dynamic filtering** with `web_search_20260209` plus `code_execution_20260120` lets Claude write Python that post-processes search results *before* they enter the context window — saving tokens and improving signal.\n",
    "\n",
    "**Strict mode (`strict: True`)** uses grammar-constrained sampling to force tool inputs to conform 100% to your JSON schema. No trailing commas, no missing required fields, no surprise keys. Strict schemas must declare `additionalProperties: False`.\n",
    "\n",
    "> **ZDR warning:** standard web search alone is ZDR-eligible. Adding `code_execution_20260120` for dynamic filtering makes the **entire request non-ZDR** because execution state is held server-side."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Defining the Toolbox\n",
    "\n",
    "Both `web_search` and `code_execution` are server-side built-ins — Anthropic runs them and feeds the results back to Claude. `add_lead_to_crm` is a **client-side** custom tool: when Claude calls it, your code is responsible for executing the action and returning the result.\n",
    "\n",
    "Note the explicit `name` field on every entry. `code_execution_20260120` requires `name` even though it's a built-in server tool — omitting it causes a 400 `BadRequestError`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# DEFINE THE TOOLS\n",
    "# Note: Use 'strict: True' for the CRM tool to guarantee valid JSON.\n",
    "# Note: 'code_execution' must have a name even if it's a built-in server tool.\n",
    "\n",
    "tools = [\n",
    "    {\n",
    "        \"type\": \"web_search_20260209\",\n",
    "        \"name\": \"web_search\",\n",
    "        \"max_uses\": 5,\n",
    "        \"allowed_domains\": [\"gartner.com\", \"forrester.com\", \"hbr.org\"],\n",
    "    },\n",
    "    {\n",
    "        \"type\": \"code_execution_20260120\",\n",
    "        \"name\": \"code_execution\"   # CRITICAL: required alongside type\n",
    "    },\n",
    "    {\n",
    "        \"name\": \"add_lead_to_crm\",\n",
    "        \"description\": \"Adds a validated prospect to the enterprise CRM. Use only after industry research is complete.\",\n",
    "        \"strict\": True,\n",
    "        \"input_schema\": {\n",
    "            \"type\": \"object\",\n",
    "            \"properties\": {\n",
    "                \"company\": {\"type\": \"string\"},\n",
    "                \"contact_email\": {\"type\": \"string\", \"format\": \"email\"},\n",
    "                \"budget_range\": {\"type\": [\"string\", \"null\"]},   # Nullable to prevent hallucinations\n",
    "            },\n",
    "            \"required\": [\"company\", \"contact_email\", \"budget_range\"],\n",
    "            \"additionalProperties\": False,\n",
    "        },\n",
    "    },\n",
    "]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. The Initial Request & `stop_reason`\n",
    "\n",
    "Send the user's prospecting goal with the toolbox attached. Don't print just the final text — inspect `stop_reason` and walk the content blocks. On the first turn you should see `tool_use`, not `end_turn`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# THE INITIAL REQUEST\n",
    "# On Opus 4.7, 'thinking' is required for complex tasks like this.\n",
    "response = client.messages.create(\n",
    "    model=\"claude-opus-4-7\",\n",
    "    max_tokens=8192,\n",
    "    thinking={\"type\": \"adaptive\"},\n",
    "    tools=tools,\n",
    "    messages=[{\n",
    "        \"role\": \"user\",\n",
    "        \"content\": \"Find the top 5 AI consulting prospects in EMEA finance and add them to my CRM.\",\n",
    "    }],\n",
    ")\n",
    "\n",
    "# SHOW OUTPUT\n",
    "print(f\"Stop Reason: {response.stop_reason}\")   # Expected: 'tool_use'\n",
    "for block in response.content:\n",
    "    if block.type == \"thinking\":\n",
    "        print(\"Reasoning Signature Detected.\")\n",
    "    if block.type == \"tool_use\":\n",
    "        print(f\"Claude requested tool: {block.name}\")\n",
    "        print(f\"Parameters: {block.input}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Handling the Agentic Loop\n",
    "\n",
    "Web search runs **server-side** — Anthropic executes it and feeds the results back to Claude inside the same request. Your CRM write is **client-side** — your code must execute it and return a `tool_result` block before Claude can finish its turn.\n",
    "\n",
    "The full flow:\n",
    "\n",
    "1. **Turn 1** — Claude calls `web_search`. The API runs it server-side and silently injects results into Claude's context.\n",
    "2. **Turn 2** — Claude reasons over the results and emits a `tool_use` block for `add_lead_to_crm`.\n",
    "3. **Turn 3** — your application performs the database write and sends a `tool_result` block back. Claude generates the final `end_turn` response.\n",
    "\n",
    "Skeleton for the client-side leg:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Pseudocode skeleton for Turn 3.\n",
    "# Replace `crm.write(...)` with your actual CRM client call.\n",
    "\n",
    "def handle_tool_use(block):\n",
    "    if block.name == \"add_lead_to_crm\":\n",
    "        # crm.write(block.input)  # your client-side action\n",
    "        return {\n",
    "            \"type\": \"tool_result\",\n",
    "            \"tool_use_id\": block.id,\n",
    "            \"content\": \"ok: lead written\",\n",
    "        }\n",
    "    raise ValueError(f\"unhandled client-side tool: {block.name}\")\n",
    "\n",
    "# Then loop:\n",
    "# while response.stop_reason == \"tool_use\":\n",
    "#     tool_results = [handle_tool_use(b) for b in response.content if b.type == \"tool_use\"]\n",
    "#     response = client.messages.create(\n",
    "#         model=\"claude-opus-4-7\", max_tokens=8192, thinking={\"type\": \"adaptive\"}, tools=tools,\n",
    "#         messages=[\n",
    "#             {\"role\": \"user\", \"content\": \"Find the top 5 AI consulting prospects ...\"},\n",
    "#             {\"role\": \"assistant\", \"content\": response.content},\n",
    "#             {\"role\": \"user\", \"content\": tool_results},\n",
    "#         ],\n",
    "#     )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. Final Verification & Citations\n",
    "\n",
    "Once the loop terminates with `end_turn`, inspect the final text blocks. Web search automatically attaches citations to grounded claims — verify them so you can audit the sources Claude relied on."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# FINAL OUTPUT VERIFICATION\n",
    "# Web search automatically provides citations.\n",
    "for block in response.content:\n",
    "    if block.type == \"text\":\n",
    "        print(\"Final Answer:\", block.text)\n",
    "        if hasattr(block, \"citations\") and block.citations:\n",
    "            print(\"Sources:\", [c.url for c in block.citations])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Architectural Summary\n",
    "\n",
    "- **Safety:** nullable types in the schema (`[\"string\", \"null\"]`) prevent Claude from inventing a budget figure when one isn't in the search results.\n",
    "- **Efficiency:** `code_execution_20260120` is **free** when bundled with `web_search` or `web_fetch`, so dynamic filtering costs nothing extra in token generation.\n",
    "- **Compliance:** that same bundling makes the request non-ZDR. Standard web search alone stays ZDR-eligible.\n",
    "- **Selection:** tool descriptions, not names alone, drive Claude's routing. Disambiguate any tools whose purposes overlap."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
