diff --git a/multimodal/crop_tool.ipynb b/multimodal/crop_tool.ipynb new file mode 100644 index 00000000..2685015a --- /dev/null +++ b/multimodal/crop_tool.ipynb @@ -0,0 +1,771 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Giving Claude a Crop Tool for Better Image Analysis\n", + "\n", + "When Claude analyzes images, it sees the entire image at once. For detailed tasks—like reading small text, comparing similar values in a chart, or examining fine details—this can be limiting.\n", + "\n", + "**The solution:** Give Claude a tool that lets it \"zoom in\" by cropping regions of interest.\n", + "\n", + "This notebook shows how to build a simple crop tool and demonstrates when it's useful." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## When is a Crop Tool Useful?\n", + "\n", + "- **Charts and graphs**: Comparing bars/lines that are close in value, reading axis labels\n", + "- **Documents**: Reading small text, examining signatures or stamps\n", + "- **Technical diagrams**: Following wires/connections, reading component labels\n", + "- **Dense images**: Any image where details are small relative to the whole" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "%pip install -q anthropic pillow datasets" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [], + "source": [ + "import base64\n", + "from io import BytesIO\n", + "\n", + "from anthropic import Anthropic\n", + "from datasets import load_dataset\n", + "from IPython.display import Image, display\n", + "from PIL import Image as PILImage\n", + "\n", + "client = Anthropic()\n", + "MODEL = \"claude-opus-4-5-20251101\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load an Example Chart\n", + "\n", + "We'll use a chart from the FigureQA dataset to demonstrate." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Question: Is Cyan the minimum?\n", + "Answer: Yes.\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Load a small subset of FigureQA\n", + "dataset = load_dataset(\"vikhyatk/figureqa\", split=\"train[:10]\")\n", + "\n", + "# Helper to convert dataset images to PIL\n", + "def get_pil_image(img) -> PILImage.Image:\n", + " if isinstance(img, PILImage.Image):\n", + " return img\n", + " if isinstance(img, dict) and 'bytes' in img:\n", + " return PILImage.open(BytesIO(img['bytes']))\n", + " raise ValueError(f\"Cannot convert {type(img)}\")\n", + "\n", + "# Get an example chart\n", + "example = dataset[3]\n", + "chart_image = get_pil_image(example['image'])\n", + "question = example['qa'][0]['question']\n", + "answer = example['qa'][0]['answer']\n", + "\n", + "print(f\"Question: {question}\")\n", + "print(f\"Answer: {answer}\")\n", + "display(chart_image)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define the Crop Tool\n", + "\n", + "The crop tool uses **normalized coordinates** (0-1) so Claude doesn't need to know the image dimensions:\n", + "- `(0, 0)` = top-left corner\n", + "- `(1, 1)` = bottom-right corner\n", + "- `(0.5, 0.5)` = center" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def pil_to_base64(image: PILImage.Image) -> str:\n", + " \"\"\"Convert PIL Image to base64 string.\"\"\"\n", + " if image.mode in ('RGBA', 'P'):\n", + " image = image.convert('RGB')\n", + " buffer = BytesIO()\n", + " image.save(buffer, format=\"PNG\")\n", + " return base64.standard_b64encode(buffer.getvalue()).decode(\"utf-8\")\n", + "\n", + "\n", + "# Tool definition for the Anthropic API\n", + "CROP_TOOL = {\n", + " \"name\": \"crop_image\",\n", + " \"description\": \"Crop an image by specifying a bounding box.\",\n", + " \"input_schema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"x1\": {\n", + " \"type\": \"number\",\n", + " \"minimum\": 0,\n", + " \"maximum\": 1,\n", + " \"description\": \"Left edge of bounding box as normalized 0-1 value, where 0.5 is the horizontal center of the image\"\n", + " },\n", + " \"y1\": {\n", + " \"type\": \"number\",\n", + " \"minimum\": 0,\n", + " \"maximum\": 1,\n", + " \"description\": \"Top edge of bounding box as normalized 0-1 value, where 0.5 is the vertical center of the image\"\n", + " },\n", + " \"x2\": {\n", + " \"type\": \"number\",\n", + " \"minimum\": 0,\n", + " \"maximum\": 1,\n", + " \"description\": \"Right edge of bounding box as normalized 0-1 value, where 0.5 is the horizontal center of the image\"\n", + " },\n", + " \"y2\": {\n", + " \"type\": \"number\",\n", + " \"minimum\": 0,\n", + " \"maximum\": 1,\n", + " \"description\": \"Bottom edge of bounding box as normalized 0-1 value, where 0.5 is the vertical center of the image\"\n", + " },\n", + " },\n", + " \"required\": [\"x1\", \"y1\", \"x2\", \"y2\"]\n", + " }\n", + "}\n", + "\n", + "\n", + "def handle_crop(image: PILImage.Image, x1: float, y1: float, x2: float, y2: float) -> list:\n", + " \"\"\"Execute the crop and return the result for Claude.\"\"\"\n", + " # Validate\n", + " if not all(0 <= c <= 1 for c in [x1, y1, x2, y2]):\n", + " return [{\"type\": \"text\", \"text\": \"Error: Coordinates must be between 0 and 1\"}]\n", + " if x1 >= x2 or y1 >= y2:\n", + " return [{\"type\": \"text\", \"text\": \"Error: Invalid bounding box (need x1 < x2 and y1 < y2)\"}]\n", + " \n", + " # Crop\n", + " w, h = image.size\n", + " cropped = image.crop((int(x1*w), int(y1*h), int(x2*w), int(y2*h)))\n", + " \n", + " return [\n", + " {\"type\": \"text\", \"text\": f\"Cropped to ({x1:.2f},{y1:.2f})-({x2:.2f},{y2:.2f}): {cropped.width}x{cropped.height}px\"},\n", + " {\"type\": \"image\", \"source\": {\"type\": \"base64\", \"media_type\": \"image/png\", \"data\": pil_to_base64(cropped)}}\n", + " ]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's test the crop tool manually:" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cropped to (0.00,0.00)-(0.40,0.35): 167x140px\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Crop the legend area (top-right of most charts)\n", + "result = handle_crop(chart_image, x1=0.0, y1=0.0, x2=.4, y2=0.35)\n", + "print(result[0]['text'])\n", + "display(Image(data=base64.b64decode(result[1]['source']['data'])))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The Agentic Loop\n", + "\n", + "Now we connect everything: send the image to Claude with the crop tool available, and handle tool calls in a loop until Claude provides a final answer." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "def ask_with_crop_tool(image: PILImage.Image, question: str) -> str:\n", + " \"\"\"Ask Claude a question about an image, with the crop tool available.\"\"\"\n", + " \n", + " messages = [{\n", + " \"role\": \"user\",\n", + " \"content\": [\n", + " {\"type\": \"text\", \"text\": f\"Answer the following question about this image.\\n\\nThe question is: {question}\\n\\n\"},\n", + " {\"type\": \"image\", \"source\": {\"type\": \"base64\", \"media_type\": \"image/png\", \"data\": pil_to_base64(image)}},\n", + " {\"type\": \"text\", \"text\": f\"\\n\\nUse your crop_image tool to examine specific regions including legends and axes.\"},\n", + " ]\n", + " }]\n", + " \n", + " while True:\n", + " response = client.messages.create(\n", + " model=MODEL,\n", + " max_tokens=1024,\n", + " tools=[CROP_TOOL],\n", + " messages=messages\n", + " )\n", + " \n", + " # Print assistant's response\n", + " for block in response.content:\n", + " if hasattr(block, 'text'):\n", + " print(f\"[Assistant] {block.text}\")\n", + " elif block.type == \"tool_use\":\n", + " print(f\"[Tool] crop_image({block.input})\")\n", + " \n", + " # If Claude is done, return\n", + " if response.stop_reason != \"tool_use\":\n", + " return\n", + " \n", + " # Execute tool calls and continue\n", + " messages.append({\"role\": \"assistant\", \"content\": response.content})\n", + " \n", + " tool_results = []\n", + " for block in response.content:\n", + " if block.type == \"tool_use\":\n", + " result = handle_crop(image, **block.input)\n", + " # Display the cropped image\n", + " for item in result:\n", + " if item.get(\"type\") == \"image\":\n", + " display(Image(data=base64.b64decode(item[\"source\"][\"data\"])))\n", + " tool_results.append({\n", + " \"type\": \"tool_result\",\n", + " \"tool_use_id\": block.id,\n", + " \"content\": result\n", + " })\n", + " \n", + " messages.append({\"role\": \"user\", \"content\": tool_results})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo: Chart Analysis\n", + "\n", + "Let's ask Claude to analyze our chart. Watch how it uses the crop tool to examine specific regions." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Question: Is Cyan the minimum?\n", + "\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Claude's analysis:\n", + "\n", + "[Assistant] I'll help you answer whether Cyan is the minimum in this pie chart. Let me examine the image more closely.\n", + "[Tool] crop_image({'x1': 0.0, 'y1': 0.0, 'x2': 0.3, 'y2': 0.3})\n", + "[Tool] crop_image({'x1': 0.3, 'y1': 0.3, 'x2': 0.7, 'y2': 0.7})\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Assistant] Based on my examination of the pie chart, I can see the relative sizes of each segment:\n", + "\n", + "1. **Royal Blue** - appears to be the largest segment, taking up roughly 35-40% of the pie\n", + "2. **Peru** (brown/orange) - appears to be the second largest, roughly 25-30%\n", + "3. **Red** - appears to be third, roughly 20%\n", + "4. **Light Slate** (gray) - appears to be fourth, roughly 15%\n", + "5. **Cyan** - appears to be the smallest segment, roughly 5-10%\n", + "\n", + "**Answer: Yes, Cyan is the minimum.** \n", + "\n", + "Cyan has the smallest slice in the pie chart, making it the minimum value among all five categories shown.\n", + "\n", + "Ground truth: Yes.\n" + ] + } + ], + "source": [ + "print(f\"Question: {question}\\n\")\n", + "display(chart_image)\n", + "\n", + "print(\"\\nClaude's analysis:\\n\")\n", + "ask_with_crop_tool(chart_image, question)\n", + "\n", + "print(f\"\\nGround truth: {answer}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Try Another Example" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Question: Is Forest Green greater than Medium Orchid?\n", + "\n" + ] + }, + { + "data": { + "image/png": "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Pzig3KRB/cYbb3QKjS4hDfaQQw6xPfbYw03XvPHGG2lcOfurSnlp3Lix7bzzzq6zusmfeeYZ03TMqaeear169cp+IdLooX64zjrrLDviiCOKvMro0aOL3B+WnZp26datmx155JGFRNKT6rXXXmt33nln6JWb8847z1q0aOHk1xTt888/bxMnTrRDDz3UbrnlFqtSpUohNmF7E3X5GzZsaFdccUVsWB988MHYe1n2w16kxOg3UEquV/S9eNhhh9m6detMrgn6TfSroNykSP7ss89OsWY4qtWpU8cJ8uWXX9rTTz/t/C569uxpw4YNM+9YOCQtWgpNR2papriyfv364g6FYv+KFSusZcuWRcoiK45M1GEvUmxkuZNS8/777ztF9+GHH/bdHJ8p7lGXP5GzftC9oilaWfLDXPQZj1ds4mWVu4IeeP0sf9iS/exBCNqeOXOmzZgxw03HaM4xKkUfYD2l77rrrs5qc/7550dCsdH4yvysJ5einEY/++wz01NdmEujRo2cr01xMspkH4Vy99132w8//GDjxo1znwW//QwyzTzK8mvhyMKFCx1yTUPpPpD/yeeff26vvPKK6TMS5hI/7fruu+8WElXTs34rdyg3hYYktTd77bWX+2KTmVYOlRMmTEjtQgGrJeuFPsyy1GhKqqTVUwETrdTuaspBflWyVGmKRqun5GchJ0PNNZ900kmlXiPIJ7Rt29YmT57sfGvi5ZBFR1NyUbDeSW75Fmhq7rnnnnNTsvEsorAdZfm7d+9uV199tV144YV27rnnWr9+/ZxiI2daWS522223UN8CW7dudVNPUvBmzZoVk1Xv5Yez5557xvb5sUFuqTSoa3BlsZG1Zvfdd7d27do55cZzLk50NkyjqayrKs1cipznQLdq1Sp79dVXTc7Up59+urPmZF2ny7hDenLTE/vrr78eCwugJ/eBAwfafvvtV8atZd/lNPUm3zL5HnlFTvZaDqovfn0mwlzkU+CtCNQKyTfffNPmz59vxx13nPsuCLPski3q8ouBHmh0v2uK1vO/0v4oFE07SZFTqVu3rj3++OP266+/2gMPPOAebvr37+8rBpSbFPFrEPUjpid4DeySJUusTZs2NnTo0BSvGKxqWgavL/PED7TMs++8844df/zxdtFFFwVLqCR7KzO0fI6k1MpEq2W/mqKLUpH8elKTY3mHDh18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uYnSaCMrBCAAAQhAIAIEUG4iMMiICAEIQAACEIgSAZSbKI02skIAAhCAAAQiQADlJgKDjIgQgAAEIACBKBFAuYnSaCMrBCAAAQhAIAIEUG4iMMiICAEIQAACEIgSAZSbKI02skIAAhCAAAQiQADlJgKDjIgQgAAEIACBKBFAuYnSaCMrBCAAAQhAIAIEUG4iMMiICAEIQAACEIgSAZSbKI02skIAAhCAAAQiQADlJgKDjIgQCCKBX375xc466yxbtWpVyt0fM2aMnXbaafbPf/5zh66R7PmJF023fuL1eA8BCKRGAOUmNW7UggAEyplA7dq17bLLLrP69eun3NKll15qFStWtFatWu3QNZI9P/Gi6dZPvB7vIQCB1Aig3KTGjVoQgEA5E6hRo4YdccQRabWyePFi+/XXX22vvfbaoeske37iRdOtn3g93kMAAqkRqJxaNWpBAAJRJ/DJJ5/Y/fffb+vWrbN+/fpZ69atbfjw4bbLLrvYxRdfbA0aNLBHHnnEtm3bZhUqVLCNGzfawIEDLScnx0qrO3/+fBs/fry1adPGrrvuOsvLy7PHH3/cli9f7rDr9bzzzrMDDzywxGGYM2eO7b777la9enV33syZM+21116zqlWr2qZNm1xfrrrqKtc/naDzmzdvbq+88op9+eWXtmjRIjetpaktFfVDx77++murXLmyU5wGDx7sZNbxxPa0jwIBCGSeAMpN5pnTIgRCQeCQQw6xQYMGOeXjgAMOsLVr11rHjh3tyiuvdPLNmzfPevbsafvuu697/+STT9obb7xh5557rpVWV3WmTp1q++yzj6v70ksv2fr1623YsGGxa8kiU1qRshE/JSVFbMCAAbbTTju5qur/7Nmzbf/993fvdb7O6dq1q/P3+eyzz+z666+3I4880k2P3XfffU6huemmm9x01+jRo+3RRx91DHSBxPbcRfkPAhDIOAGUm4wjp0EIhIeALCdSHu69915r2LChDRkyJCacrCUvvviivfDCC27fggUL7Oijj44dL6lufn6+zZ07184//3x3/sKFC+377793FhNZiGS12ZEiZeOEE06InVqzZk2TgiILjIoUsK1bt8aO63wpZLm5uW5fy5YtTX356aefbMmSJfb222/bs88+6xQbndC4cWN79913C9WPby92gA0IQCCjBFBuMoqbxiAQPgInnniijRw50mSZkfOuihSBq6++2oYOHWqdOnVy++Rsqymi+FJUXR3XdJCmsfbYYw93+kUXXWRPPPGEXXvttVavXj3T+6OOOir+UtttS2nR9JZnuZk8ebI98MADNmrUKKe8yBJ0xhlnxPrknS/LjleWLVvmNnfeeWd7/fXXTU7OY8eO9Q7bmjVrbM8993Tvvfpee7GT2IAABDJOAOUm48hpEALhIaDl2p9++qlTQiZMmGBXXHGFE+6tt96y3XbbLabYaApJlpf4H/7i6uoCstrIalKlShXbvHmz1alTx6655hp3/XHjxjlfn9KUm//9739WqVIl50Oja7788st2zDHHxKwyX331lfMLkuKiovO3bNkSO659H3/8sVN+mjRpYlJ0OnToEJuC0nEpYGpDJbE9t5P/IAABXwiwWsoX7DQKgeATkOVDioYcchWP5s0333SWDEn222+/uekcT8rnnnvO6tatG1McSqqrOt9++21MEdJ1NR2komklKU2amiqtaIpp7733jlmT4vskJUYOy/HKls5X0RSUiqbC5Hzcv39/917K1tKlS2NyKf6OnIl1XZXE9txO/oMABHwhgOXGF+w0CoFgE9CPvqZpGjVq5KZqZs2a5SwsN9xwg/O7kd/K7bff7lZHSRlZuXKlWzX1/PPPOwWlpLry05Ejr1ZVTZs2zWRZkSKiFUoq1apVK2Q9KYqknHwnTZrkfGv+9a9/2UknneT8dB566CE35aVry3FYTtAfffSRU6Z0/vHHH++mvzZs2ODq3njjjSZnaZU///nPTg45GKtP8sWRg7OmyYpqz1XiPwhAwBcCFQo+oDcXtDzcl9ZpFAIQgAAEIAABCJQxAaalyhgol4MABCAAAQhAwF8CTEv5y5/WIQCBFAh4y7KLqtqjRw83XVbUMfZBAALRIMC0VDTGGSkhAAEIQAACkSHAtFRkhhpBIQABCEAAAtEggHITjXFGSghAAAIQgEBkCPx/KYVlOOTXjQoAAAAASUVORK5CYII=", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Claude's analysis:\n", + "\n", + "[Assistant] I'll help you answer whether Forest Green is greater than Medium Orchid. Let me first examine the image to identify these colors and their values.\n", + "[Tool] crop_image({'x1': 0, 'y1': 0, 'x2': 0.3, 'y2': 1})\n", + "[Tool] crop_image({'x1': 0, 'y1': 0.85, 'x2': 1, 'y2': 1})\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAKoAAAGQCAIAAACxgmyqAAAbeklEQVR4nO2de1QU5/n43+W2sNxX7hcRFLkbixW8RJJUMDRGiR5aYzTWGGuTWq02UY/GitpYbKhi1eaIBJMjBi9HjokacmJRIyII0YhkoSwrCAILy3VhF3ZZ2J3fH/PrHr6gssvM7MzkfT5/eHaG8d0HP8478z7zzvsICIJAAK5YsR0AwCagH2tAP9aAfqwB/VgD+rEG9GMN6Mca0I81oB9rQD/W8Ea/Uqn83e9+t3nzZr1eP/ang4ODmzZtSkpKSkpKamhosHh0fIWj+mtra0mXGo2G3NPW1iaXy2trawcGBsYeIBQKP/74YzYj5ic2bAdgKuHh4Wlpac7Ozs7OzmzH8vOBi/pbW1szMjLIz9u2bXN2dt67d+/WrVtlMhlCKDs7WygUjjogPT19VCODg4N5eXl37txxcnLSaDSrVq1KSEiw5G/BDwhOIpVKExMTExMTBwYGyD1KpZLc8/jxY1MOyMjISExMzMnJIQhix44diYmJ9+/fZ+V34TIcvfZTRKPRXLt2DSEUHBxs/PPixYssh8U9uNj5U6e9vZ0gCIRQTk7OuXPntFqtu7t7b28v23FxDo7qFwgEVA7w8vIiP7zzzjsLFy5ECA0ODqpUKrrC+9nA0c7fycmJ/KDT6fLy8oqLi806wMHB4Ve/+hVC6N69e+SenJycb7/9ltmgeYiA4OpUz8zMzKKiIl9fXxcXl+3bt+/evZu88w8JCUlPTxeLxc8/wMHB4fTp08XFxa6uru7u7pMnT163bp21tTXbvxa34K5+wAJwtPMHLAPoxxrQjzWgH2tAP9aAfqwB/VgD+rGGizn/Dz/88C9/+QvbUWABF7N+AoFAr9dbWTHVM8nlcj8/P4YatwA0xg+dP9aAfqwB/VgD+rEG9GMN6Mca0I81XNG/b98+wf9gOxaMgLQP/4C0D0APoB9rQD/WgH6sAf1YA/qxBvRjDejHGtCPNVzRD0lfVoCkL/+ApC9AD6Afa0A/1oB+rAH9WAP6sQb0Yw3oxxrQjzVc0Q9JX1aApC//gKQvQA+gH2tAP9aAfqwB/VgD+rEG9GMN6Mca0I81XNEPSV9WgKQv/4CkL0APoB9rQD/WgH6sAf1YQ3M5h/v37xcVFYlEooaGhmXLlsXFxSGELly4IJPJBAJBcHDwypUr6f1GgAp06u/s7Pzqq6/2798vEAiUSqVGo0EIVVdXFxUVHTt2DCG0ZcuWmJiY6OhoGr8UoAKd+r/55pvAwMDPP/9co9EEBQW9/vrrCKHCwsK4uDgymTNnzpzCwkLQzx3ovPY/efKkuLh45cqVf/zjH0tLS7/55huEkEKhEIvF5AFisbitrY3GbwQoQufZr9VqY2NjHRwcEEIvvfRSYWHh4sWLx/1b+/bt27t376id1tbWH3zwAXMFneRyOUMtWwa64qdTv4eHhzFjb2NjMzQ0hBDy9vbu7u4md3Z3d3t7e4/6W2lpaWlpaSP3QNL3+XA06fvyyy9XVVWRDxEkEklsbCxCaOHCheXl5QRBEARx9+7dxMREGr8RoAjNj3wuXrz46NEjR0dHg8Hw/vvv29nZIYTOnz9vHPi99dZb48cEZ/9zoTF+eOLHPzja+QO8A/RjDejHGtCPNaAfa0A/1nBFP8z0ZQUY9/MPGPcD9AD6sQb0Yw3oxxrQjzWgH2tAP9aAfqwB/VjDFf2Q9GUFSPryD0j6AvQwQf1ffPEFrWEA7PDM1zwqKyu/+uqrp/5Ir9f7+/szFRFgQZ6pv7S01M/Pj5yoPwqZTBYeHs5kVICFeKZ+X1/fpUuXPvVHxcXFkZGRjIUEWA6z7/wJgmB6bAZ3/s+HhTv/hw8f7tix44033pDL5SdPnrx9+zYtXw+wi0n6S0pKTpw4MXv27MDAQH9//5SUlDNnzuh0OqaDA5jGpBe8CwsLjx49amNjU1NTgxDy9vaOjY3VaDRPvTEEeMT4Z39LS4ufn5+trW1jY2NgYCBCiCCIzs5OV1dXGuOApC8rjK/fwcHh+++/r6ioqKysDA4OrqurO3DgwKxZs+iNIy0tjfgf9LYMPIfxO3+xWJyYmLh9+3ZSjEgkWrt2bXJyMvOxAYxj6sCvtbX18ePHIpEoNDTU0dGR2Zhg4PdcWBj4+fj4DA8Pl5SUfPrpp+fPn+/v76fl6wF2MenOX6VS7dy5s6OjIzAw0GAwlJaWXr58OTs7WyQSMR0fwCgm6c/Ozk5OTl68eDF5W67T6bZt21ZTU0Mu3gTwF5M6/9bW1tdff904JLOzswsICICHfj8DTNLv6Og4ODho3JRKpV5eXmNX6AN4h0nP+3t6elavXj19+nRbW9vOzk65XL5582YLBQgwiUnP+6dMmWLM84SEhMhkMuZGZYAl4crz/qeu7AswzQRn+mo0mubm5tDQUNoDQpD2GQ8a4zd1SWedTieVSru6usj/Lk+ePPHy8mJIP2AxTNLf0NCQlpZmMBg0Go27u3tXV5etrW1GRgbTwQFMY2ra55NPPmlpaWlubl66dOng4ODx48cnT57MdHAA04x/fW1ubg4PD/f29n706FFwcDBCSCgUqlQq5mMDGGd8/e3t7WS+r6enp6WlhSCI69ev29vbMx8bwDjjd/4eHh63b9+eO3dudHT03r17MzMzPTw8MjMzn3W8SqVav379ihUrli9fTu6BQm6cZXz9kydPzsrKQghNnTr1+PHjSqXyhRdeeM7Zf/LkyZH5YCjkxmXMG1uHhYXFx8fb29s/6x2/oqKiqVOnjhyVGgu5CQQCspAblXABeqHzHb/e3t5bt27t3r2bnBBMolAoQkJCyM9isfjhw4eU4gVohc53/E6ePPn73//e3Km6UMhtAjBeyM3cnH93d3dnZ+f58+cRQjU1NW1tbXK5/E9/+hMUcqMdOuMnqNHf319bWztqZ3p6en5+PvlZIpFs3LjRYDAYDIbNmzdXVlaO2yZCSK/XUwzsOZDDV/5CY/xUyzhKpVIbm//TSHZ2tlQqVSgUNjY2S5cujYqKWrBgwYEDBwQCQXx8fExMDMVvBGiE6to+Fy5cSElJEQqFdAWEoPMfDw6t7aPVaul1D1gSmLSDNZT0d3Z2enh40BUKYHko6ZdKpbDID68xSX91dfXBgwebmppUKtX27dvJ5R0QQvPnzzdm9AA+YpL+s2fPLl68OCAgIDMz09HR8eOPPy4qKqI3Dni/nxXG1y+Xy8PCwmJiYqqrq6uqqrZv3x4TEzN9+nR644D3+1nBpNk+Li4u5ASvtWvXOjg4qNVqGOz9PBhff0hISG5u7sqVK728vJKTk+/cubNt27akpCQLBAcwjUlZP6VS2dnZOW3aNISQTCbT6XRRUVEMxmRm1u9xz+OypjLT21cqlW5ubqYfH+gaOD9ovunHM42l5/m7ubkZ/73Iuf3Hjh3btGkTLRFQ54H8wUfXPmKu/VdDX+WUfhp5pn6CILKyslJSUrRa7fHjx0f+yGAwwHCfRlSN2rpzCtOP12q17faNph8v8rGb/jvfp/7omfp1Ol1vb69IJCovLw8LCxvZ21RXV0dERJj+9cDzGeob7rhv3sR5FRoy/WCXqQ7P+tEz9QuFwh07diCEPD09k5KSRq7j4ufnB9menwfjX/vnzZs3ak9sbOzw8HBPT4+7uzszUQEWwqS76+Li4vLy8pF7SkpKSkpKmAkJsBwm6Sdn/arVaoSQVqvNyso6cOAAvZd/SPqygkn63dzc3n777T179ly7dm39+vVdXV1xcXFTpkyhMQ5I+rLC+Nd+g8Hw6quvCgQCOzu7jIyMTZs2PWsGMMA7xtcvk8mOHj2qUCiSk5M3btx49OhRZ2fnrq6u1NRUC8QHMMr4nb9UKrWysvrkk0/Wr18fGBi4b9++goICT09PCwQHMM34Z39XV9fBgweNyziLRKKkpCQo5fTzYPyz/5133hm1hPeiRYucnJxkMhljUQEWApZ2whpY2glrYGknrIGlnbCGK0s7QdKXFcbXTy7tVFdXFx0dfejQoeTk5FOnTq1bt47eOCDpywomLe106NCh3t7e+fPnm7K0E8AjTLr1Kysr+/HHH997772wsDCmAwIsiUlP/GpqaoKDg/fs2XPs2LGWlhamYwIshklnf0pKSkBAQGpqakVFxcmTJwUCwfLly2fMmMF0cADTjK+fIAjjAx53d/eAgICCgoJHjx55eHikpqa++OKLDEcIMMj4+h89enTjxg1/f//vvvuusbExISFh//790dHRKpXq0KFD7u7ujL7yATDK+Ppramry8/NnzJixZMmShIQE4z2/g4ODk5MT1HDnNePrl0qlp06dCggIGLW/pqZGLBbDUx9eM77+LVu2jFq6jSQmJgZWaeM74w/8nuqediDpywpcWdkLkr6swBX9ACuAfqwB/VgD+rEG9GMNnYO6oqKiW7dueXl5yeXy+Pj41157jdwPlbw4C836N27cKBaLtVrtypUro6KigoKCoJIXl6Gz8//oo4/EYjFCyN7e3tXVlazhApW8uAyd+o0Ju+bmZltbW3JCgEKhIP9PIITEYnFbWxuN3whQhP6E7uDg4IkTJ/76179aW1ubcjz1Sl5KpdLMGM1Dq9UyWvmrv8uMdZomwNDQ0LPip1m/Vqs9fPjwunXrjC+BWKCSl1uPG6Wgx8Pe3p7R4i/dXeoGxOB7E7a2ts+Kn87Of2Bg4J///Ofq1atDQkIaGhoKCgoQQgsXLiwvLyeT+Xfv3k1MTKTxGwGK0Hn2HzlypKysrLKyEiGk1+tXrVqFEIJKXlyGTv27du166v4VK1bQ+C0AjUDWD2tAP9aAfqwB/VgD+rEG9GMNV/TDTF9W4Ip+mOnLClzRD7AC6Mca0I81oB9rQD/WgH6sAf1YA/qxBvRjDVf0Q9KXFbiiH5K+rMAV/QArgH6sAf1YA/qxBvRjDejHGtCPNaAfa0A/1nBFPyR9WYEr+iHpywpc0Q+wAujHGtCPNaAfa0A/1oB+rAH9WAP6sQb0Yw1X9EPSlxW4oh+SvqzAFf0AK4B+rAH9WAP6sQb0Y40lqnNDITfOwrh+KOTGZRjv/KGQG5dhXD8UcuMylrj2Px/qhdwiRZGfJX9Gf2T/w9nOmdFCbnpHIuQDF+bat7ITWKiQ21gsUMjND/mFo3DTQ5LL5YwWZpsIIWYcS2P8jHf+UMiNyzB+9kMhNy5jiWs/FHLjLJD1wxrQjzWgH2tw1H/48GG2Q6AEjfELODi5ytxx/wTa5+BvbTo0xs9+1m8sc+fOtba2ZvQr+D6hlK74+X0eTAw4+43geO0HjIB+rAH9WAP6sQZH/WOnF/ALGuPn9z0wQBEcz37ACOjHGtCPNaAfazDSX1dXNzQ0NGqnXq+XSqWsxGMuPT09CoVi7P4HDx5MuE0uPvJhiLt370okEgcHh5E7dTpdW1tbWFgYW1GZTktLy82bN8eGeu/evV/84hcTaxMj/Qih4uJiGxsbhFBbW5urq6uDg8Pg4GBkZCTbcZmKk5OTu7u7cVMqlebn5wcFBU24Qbz0796929XVFSF07ty52bNnT506VaFQXLlyhe24TCIoKMjX13fSpEkIoaqqqjNnzlRXV6ekpKSmpk64TYz0r1q1iu0QKOHs7IwQ+umnn3Jzc2tra994441du3aROycMRrd+RioqKsrLy/Pz87u6utiOxTw0Gs2OHTt8fHzOnDmzdu1aiu4Rnknf/v5+tVqNEBoeHr569aq1tfX69evZDspUOjs7L126pNfrly1bNvaNOXPBS//w8HB5eXlXV9eUKVNiYmK0Wi15DzjyfoqzdHR0aLXawMBAhFB3d/elS5cGBweXL1/u4+Mz4TYx0q9Wq7du3Wpra+vi4tLa2hoVFbV9+3a2gzIDiURSUFAQHBxs3NPb2/vdd98lJye/++67E2sTo1u/K1eufPDBB+Hh//9d4LNnz9bU1Bg3eUFbW5vBYBi5Z9asWfX19RNuECP9/f39I3MmM2fO7OzsZDEec3F1dd2wYcPY/69Xr16dcJsYdf63bt2qrKxcsmSJs7NzW1vbl19+uXXrVk9PT7bjMo+ffvqpoaFBLBbHx8eTKSwqYKQfIZSXl5efn9/X1+fv7//+++/Hx8ezHZF5pKeny2QyvV7v6Og4PDx8+PBhJycnSi0S2PDjjz9qtVqCIIaGhgiCqKmpkcvlbAdlBhKJJC8vjyCIM2fOKJXK6urqs2fPUmwTo2t/dXX1119/bWtrS262t7e/9957vr6+7EZlOgqFYmR3FR4eXlJSQrFNjPQjhObOnUs+8WttbdVqtQEBAWxHZAahoaFXr17dsGEDQqiysrK4uPill16i2CZG+mfOnDlt2jShUEhu2tnZNTQ08GixmcDAwPDw8OvXrwuFwtzc3CVLlsybN49imxjl/KOioozu1Wp1WVkZvwZ+CKGXX37Z09PTzs7u7bffXrRoEfUG8brzNyZ9fX19rayspk+fTvXO2YIMDw/v3Lmzvb3d19e3r6+vr68vMzOT4sAVo86fTPoKhUKlUmlnZxcaGhobG8t2UGZw//79iIiIjIwMcvOHH364fPnyhNO9JBh1/pcvX96yZcvx48fnzZt36tSpkJCQ6upqtoMyA4PBMHJiUlRUFNlzq1Sq4eHhibWJ0dk/MDAQFRVl3JwxY4ZxuVFeEB0dnZmZOTg46OnpqVarb9y4MX/+/P/+9783btxITU2d2MNfjPT7+fkVFRUlJCTodLqSkpL8/Pxdu3axHZQZNDY21tfXd3R0GPdcuHCB/GBMZpgLRrd+BoPh9OnTL774Yl5enlKpXLNmzcyZM9kOygwaGhrUajW91RAw0s935HJ5f39/aGgouTkwMCCRSOLi4qi0iVHnr1Qqy8vL582bZ29vX1ZWFhwczLl1vZ9Ld3f3Z599Zhzp6XS6iIgIZ2fnS5cuTfgqhpH+a9euWVtb29rafvHFFxKJxMbGZteuXcZSE7wgIiIiIiLCuBkcHOzj47N27dqJt0j1ORR/yM7OJghiaGjot7/9bW9vr1QqvXXrFttBmYFare7p6aG3TYzG/dbW1l1dXTdv3pw2bRo53c/R0ZHtoMzA0dHRzc3NuKlWq4uKiii2iVHnv2jRoj//+c8qlerAgQMymezq1at/+9vf2A7KDAiC+PzzzysrKzUaDULIYDAsXLiQYpt43fkTBKHX66nPkWKFsrKyy5cvv/nmmyKRiNwzadKkkf3BBODlP8SEEQgERvcSicTNzY1fj/wXLFhgfEJNEATZDVABI/0ymSw7O7uvr4/s8Pr7+21sbIRCYVZWFtuhmURUVNS//vUva2trT09PgUCgUqmam5vffPNNKm1ipL+wsHD16tXG27179+5NmjRp5FsTHKehoaGxsbG3t9e459e//jXFNjG69p8/f97Ozm7ZsmXk5s2bN318fEYOozlOU1OTWq2mN2CM9BMEUVtba3zTgywZwKOV3RUKxcDAgLG7gqSveQgEgpFv+TBdMoB2Ojo6xiZ9QT9GGJO+arW6tLR01qxZFBvEqPMn0el01dXV3d3dbm5uIyd/cp/+/v7h4WFycRqEUFNT0/Xr1ykl/HE7++/du/ePf/xDo9G4ubn19fXZ2dl9+OGHc+bMYTsuk3j8+HFVVRW5iJder799+zb16xdG+tva2nJycvbs2WPMnNTW1n766af+/v7kmgnc59tvvy0uLkYIqdVqkUiUnp5OsUGMOv/c3Nw5c+YYp0uQyOXygoICXizu0tzcPPId9Zs3b4pEIopvqWL0xK+7u3vatGmjdvr5+fX397MSj7lYWVmNLG4XEBBQXl5OsU2MOn+RSKTRaEQi0YMHDwICAsgR1MDAAF+eAHV3dx85coS89dPr9c3NzTt37qTYJj9+c1oICgq6cuVKfHx8VVWVu7u7p6dnb2/v8ePHefSyxyuvvPLCCy8ghGxsbPz9/WFhNzPQarXvvvuuVqtFCB05ciQwMDArK6unp+cPf/gDL1b2GhoaMhgM9I5UMdIPjAWjW78HDx4MDg4aN6VSaWtrK4vxcAGMrv18X92DCTDSj3i+ugcJQRAymaylpcXe3j4yMtKYA54YGOnn++oeCKH29vb9+/fX1dWJxWKtVqvValetWvXWW29NuEGM9I98vZdc3YPiPEkLYzAY/v73v//mN79ZsGABmf/p6OjIycn5z3/+k5SUNLE28brz5/XqHg8fPmxoaEhJSRm502AwHDx4EF7yGh++r+7R3t4+derUUTutrKxGVScyC4wGfnxf3cPT03Ps6s0Gg4HKMwuM9I9d3aOnp4fFeMwlJibm+vXrd+7cMV6vu7u7Dx06RKaBJwZG1/6CggInJ6eEhIQjR47ExcWRq3uQlZH4Qmtr6969e1taWiZNmqTVavv6+lJTU6ms7oSRfr6v7kFCEER1dXVzc7NQKIyOjvbw8KDSGkb6+Q5M9KbKqFo+bIdjHjDRmxJjB37Up0tYGONEb5VKdfv2beoTvTFa3ePLL7+USCQEQfz73/8mCOLcuXNVVVVsB2UG7e3tT548MW7W1dXl5uZSbBOjs5/vyzqOWr63sLAQXvA2A74v60hirOUze/ZsKhX8SDDSn5ycfPr0aT8/v76+vosXL65Zs4Zfg36EUHp6ulQq9fHx6evrGxoaOnLkCMUGMRr4PXjwIDIy0vjAVyqVuri48Gi6R1VVlUQiWbFixVM3JwZGZz/fZ/u0t7fPnj3buBkVFVVaWkqxTYz0oxGzfdrb23U6Hb9m+0RGRubk5GzYsMHDw2NgYODrr7+mPkEZI/2vvfaaq6ur8UUZoVBYX19P5XmJhfH29l6+fHljY6OHh0dnZ6dAIHjllVcotomR/mvXrs2dO5ec3zEwMFBRUeHl5cV2UGagUCiEQiFZxHXy5MmTJ08m99+/fz80NNTFxWUCbWKkf3h4eNu2beTZb29vP3PmzF/+8pdsB2UGo5K+Rpqamvbt2wf6x8HT0/PUqVP8Wsh1FCNX9ygpKUlISLC3t0cITXjSIkYDP76j1WrJ6r3k5pMnT77//vs1a9ZQaROjs5/v1NfXNzc3kxesoaGhkpIS8n1FKoB+3mAwGE6cOEGmrWxsbKZMmbJ582aKbULnzxvq6+v7+vronaGE0VRPvmNvbz/yvnVgYABW98AIqOWDO7TX8oFrP2/o7+8fGhqi971E0M8nCIKoqKhoampyd3ePi4ujvtAL6OcNTBRwB/28oaysrKqqat26deTmDz/8UFlZCQXccQEKuGMNFHDHGijgjjVQwB2gGbj1wxrQjzWgH2tAP9aAfqwB/VgD+rEG9GMN6Mca0I81oB9rQD/WgH6sAf1YA/qxBvRjDejHGtCPNaAfa0A/1oB+rAH9WAP6sQb0Yw3oxxrQjzWgH2tAP9aAfqwB/VgD+rEG9GMN6Mca0I81oB9rQD/WgH6sAf1YA/qxBvRjDejHGtCPNf8PYeJ9AmkSdQcAAAAASUVORK5CYII=", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Assistant] Based on the chart, I can see:\n", + "\n", + "- **Forest Green** (the first bar on the left, dark green color) has a value of approximately 35\n", + "- **Medium Orchid** (the second bar, purple/orchid color) has a value of approximately 34\n", + "\n", + "**Answer: Yes, Forest Green is greater than Medium Orchid.**\n", + "\n", + "Forest Green has a slightly higher value (around 35) compared to Medium Orchid (around 34), though the difference is quite small.\n", + "\n", + "Ground truth: Yes.\n" + ] + } + ], + "source": [ + "# Try a different chart and question\n", + "example2 = dataset[6]\n", + "chart2 = get_pil_image(example2['image'])\n", + "q2 = example2['qa'][2]['question']\n", + "a2 = example2['qa'][2]['answer']\n", + "\n", + "print(f\"Question: {q2}\\n\")\n", + "display(chart2)\n", + "\n", + "print(\"\\nClaude's analysis:\\n\")\n", + "ask_with_crop_tool(chart2, q2)\n", + "\n", + "print(f\"\\nGround truth: {a2}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "The crop tool pattern is simple but powerful:\n", + "\n", + "1. **Define a tool** that takes normalized bounding box coordinates\n", + "2. **Return the cropped image** as base64 in the tool result\n", + "3. **Let Claude decide** when and where to crop\n", + "\n", + "This works because Claude can see the full image first, identify regions that need closer inspection, and iteratively zoom in." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Alternative: Using the Claude Agent SDK\n", + "\n", + "The [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk-python) provides a cleaner way to define tools using Python decorators and handles the agentic loop automatically." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "%pip install -q claude-agent-sdk" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, create_sdk_mcp_server, tool\n", + "\n", + "# Working directory for the tool to resolve relative paths\n", + "tool_working_dir: str | None = None\n", + "\n", + "\n", + "@tool(\n", + " \"crop_image\",\n", + " \"Crop an image by specifying a bounding box. Loads the image from a relative filepath.\",\n", + " {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"image_path\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Relative path to the image file (e.g., 'chart.png')\"\n", + " },\n", + " \"x1\": {\n", + " \"type\": \"number\",\n", + " \"minimum\": 0,\n", + " \"maximum\": 1,\n", + " \"description\": \"Left edge of bounding box as normalized 0-1 value, where 0.5 is the horizontal center of the image\"\n", + " },\n", + " \"y1\": {\n", + " \"type\": \"number\",\n", + " \"minimum\": 0,\n", + " \"maximum\": 1,\n", + " \"description\": \"Top edge of bounding box as normalized 0-1 value, where 0.5 is the vertical center of the image\"\n", + " },\n", + " \"x2\": {\n", + " \"type\": \"number\",\n", + " \"minimum\": 0,\n", + " \"maximum\": 1,\n", + " \"description\": \"Right edge of bounding box as normalized 0-1 value, where 0.5 is the horizontal center of the image\"\n", + " },\n", + " \"y2\": {\n", + " \"type\": \"number\",\n", + " \"minimum\": 0,\n", + " \"maximum\": 1,\n", + " \"description\": \"Bottom edge of bounding box as normalized 0-1 value, where 0.5 is the vertical center of the image\"\n", + " },\n", + " },\n", + " \"required\": [\"image_path\", \"x1\", \"y1\", \"x2\", \"y2\"]\n", + " },\n", + ")\n", + "async def crop_image_tool(args: dict):\n", + " \"\"\"Crop tool that loads images from a filepath.\"\"\"\n", + " global tool_working_dir\n", + " \n", + " image_path = args[\"image_path\"]\n", + " x1, y1, x2, y2 = args[\"x1\"], args[\"y1\"], args[\"x2\"], args[\"y2\"]\n", + "\n", + " if not all(0 <= c <= 1 for c in [x1, y1, x2, y2]):\n", + " return {\"content\": [{\"type\": \"text\", \"text\": \"Error: Coordinates must be between 0 and 1\"}]}\n", + " if x1 >= x2 or y1 >= y2:\n", + " return {\"content\": [{\"type\": \"text\", \"text\": \"Error: Invalid bounding box (need x1 < x2 and y1 < y2)\"}]}\n", + "\n", + " # Resolve relative paths against working directory\n", + " path = Path(image_path)\n", + " if not path.is_absolute() and tool_working_dir:\n", + " path = Path(tool_working_dir) / image_path\n", + "\n", + " # Load image from path\n", + " try:\n", + " image = PILImage.open(path)\n", + " except FileNotFoundError:\n", + " return {\"content\": [{\"type\": \"text\", \"text\": f\"Error: Image not found at {path}\"}]}\n", + " except Exception as e:\n", + " return {\"content\": [{\"type\": \"text\", \"text\": f\"Error loading image: {e}\"}]}\n", + "\n", + " w, h = image.size\n", + " cropped = image.crop((int(x1*w), int(y1*h), int(x2*w), int(y2*h)))\n", + "\n", + " # Return using MCP image format (data + mimeType, not Anthropic API source format)\n", + " return {\n", + " \"content\": [\n", + " {\"type\": \"text\", \"text\": f\"Cropped {image_path} to ({x1:.2f},{y1:.2f})-({x2:.2f},{y2:.2f}): {cropped.width}x{cropped.height}px\"},\n", + " {\"type\": \"image\", \"data\": pil_to_base64(cropped), \"mimeType\": \"image/png\"},\n", + " ]\n", + " }\n", + "\n", + "\n", + "# Create an MCP server with our tool\n", + "crop_server = create_sdk_mcp_server(\n", + " name=\"crop-tools\",\n", + " version=\"1.0.0\",\n", + " tools=[crop_image_tool],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Question: Is Cyan the minimum?\n", + "\n", + "[Assistant] I'll first read the image to understand its content, then examine specific regions if needed.\n", + "[Tool] Read({'file_path': 'chart.png'})\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAaIAAAGQCAYAAADsuWTJAABE+UlEQVR4nO3deZgU1dk28Lt6ne6efRhmZRFUkE0gqEQUd0VwjaCoL5iAghAVXJCAsghEVJAPX9+4S8SEQIJIUDQqatyixoXVBRBkGWD2vXt6q676/hgY2Qam11Nddf+ui0uc6ap6Boa+55w69RxJVdULAFwIIiIiASxoDqHZYssgIiKjMokugIiIjI1BREREQjGIiIhIKAYREREJxSAiIiKhGERERCQUg4iIiIRiEBERkVAMIiIiEsoS7gE/vnw7vOU7Ir6gI+9UnDH2pYiPJyIifQl7RBRNCMXieGqbjRs34vrrr8dzzz13wtd9//33GDNmDC677DLccMMNCaqOiOgXnJpLcitXrsRll12GiRMnHvHxbdu2we12Y9OmTSd8bc+ePfHb3/42UeUSER0j7Kk5Sg7XXXcd0tPT0adPH9GlEBGdkOaCaMmSJVizZg369u2LzMxMlJaWonv37hgzZozo0jTnk08+wT//+U8AQElJCe6++27069cP55xzDhYsWID9+/cjPT0dq1atavW1rf25VlVV4aWXXsLOnTths9ngdDoxYcIEdOnSJUFfHREZheaCaMyYMfj4448xYsQI9OrVC7IsY/To0TjttNNw/vnniy5PUwYPHozy8nK88MIL6NChA55++umWz40ZMwZz585t02uPFgwGMXXqVOzduxePPfYYevfujRtvvBH33Xcfli5dioyMjLh+XURkLJq/R2SxWFBUVISSkhKUlJRgwYIFePnll/HHP/4RW7ZsAQAsXLgQQ4cOxWuvvYY//OEPGDJkCN544w0MHz4c27dvR1NTEx555BHcf//9gr+a5LBhwwbs3bsXAHDKKafAZrOhsLAQHo8H77zzjuDqiEhvNB9EtbW12LlzJ3r16oW5c+fipptuwtixY3HnnXdi7ty5CAaDeOCBB5CRkQGXy4XHHnsMd999N4YNG4b27dsDAJxOJ1eEhaGsrKzl91OnTsX48eNRV1eHrKwsVFdXC6yMiPRIc1Nzh6xcuRLr1q2D2+3GpEmTUFBQgN27d2P16tUtr8nLy0NVVRUKCgoAAP379wcADBs2TEjNIkiSFPPX5uXltfz+0UcfRW5uLgCgoaEhrOsREbWFZoPo0D2iQyorK6GqKiZMmACbzQYA8Pv9Lb8HAKvVesQ5JEmCoigAAFmWE1B14qWmpgIAAoEAAGDGjBmYNGlSVK/t27cvCgsLceDAAXzzzTe48sorIcsypk+fjjvuuANnnnlmPL4UIjIozU/NHdKuXTt07twZ69evB9AcLNOmTYOqqq0ek52djZqaGgDAzp07E1Jnog0aNAi9e/dGeXk57r//fmRnZ6O8vBxLliwBAHg8HsyePTus19rtdjz++OMYPHgw/vKXv+DBBx/ErFmzcM011zCEiCjmJFVVZwGY3dYD1j96YdQX7T/9o1Y/t3TpUqxevRr9+vXDVVddhV/96lctnyspKcGLL76I/Px8eL1eDBkyBD179sRrr72GpUuX4oILLsCtt97aMlW3ceNGvPLKKzjjjDNgtVrx3nvvYfTo0Rg6dGjUXwMREcWG5oKIiIiMJWmm5oiISJ8YREREJBSDiIiIhAo7iBx5p0Z1wWiPJyIifQn7OSJuakdERLHEqTkiIhKKQUREREIxiJKHR3QBREQx4D76Awyi5NF6LyMiouSRevQHGERERCRU2Kvm7ni0FDv3BSO+YNdiK16cXhDx8UREpC9hj4iiCaFYHE9ERPrCqTkiIhKKQUREREJpbofWJUuWYM2aNejbty+ys7NRX18PWZYxdepUuFyumF7rrbfewrJly/DAAw+0bDN+yA8//IA///nP2LdvHwYOHIhAIIBdu3Zh+PDhuPjii094LBERtZ3mRkRjxoxBZmYmRowYgUmTJmHmzJkAgNWrV8f8WsOGDUOnTp2O+7kePXpgyJAh6NChAyZNmoQpU6Zg9OjReOqpp6Cq6gmPJSKittPciOhoqqqioaEBjY2NLR9btmwZysvLkZKSAlVVMX78eLz++ut46aWXcPPNN+N3v/sd3n77bfzjH//ApEmT4PF48PHHHyMvLw8VFRW49dZbIwqRuro6ZGVlQZKkIz5eVVWFxYsXAwDmzZuHjz76CC+88AKmTp3asrX28uXLUVtbC0mSIMsyJkyYAItF83/8RERxp7kR0SErV67EwoULccsttyAQCOC6664DAHzyySfYsGED7rvvPkycOBGyLOO1117DjTfeiP79+6Nr164AgI4dO+L6669Hv3794HA4MHnyZNx+++248cYb8fzzz7e5jn379uGZZ57B448/jhUrVmDKlCnHvKZdu3a46qqrWv7/wgsvRHFxccv/f/rpp9i5cycmTpyICRMmQJZlrFmzJsI/GSIifdHsj+QjRoxAr169sHbtWjQ1NaGgoPnZo6+//ho9e/ZseV2vXr3w9ttvY+TIkbj66qvx+uuvY/DgwVi3bh3Gjx8PACgoKMCLL74Iq9UKv9+Pffv2tbmO4uJiTJw4EQCwf/9+3HPPPfjTn/6E/Pz8Np/j66+/RllZGZ566ikAQH19PWpra9t8PBGRnmk2iA4ZMmQIRo8ejXPPPRfFxcXHTIsd7te//jWeeeYZbNiwAXa7HU6nEwAwf/583HDDDbjwwgtRWVmJ9evXR1RLUVERsrKysHnz5mOCSJIkqOovXXhkWT7i8/369cPYsWMBNE83BgKBiGogItIbzU7NHWKxWHDVVVdh+fLlAIABAwbg+++/b/n8d999h7POOgsAYDKZMGzYMMydOxdDhw5teU1DQwPS09MBABUVFRHX0tTUhMrKShQVFR3zuezsbFRXVwMAgsEg9u7d2/K5AQMGYP369S1B9c4772DdunUR10FEpCeaGxEtXboU9fX1WLVqFaxWK7p164Zhw4Zh1KhRSElJwbhx41BSUoKFCxfCbrfDbDZj+PDhLcdfccUV+Pbbb9G5c+eWj40bNw5LlizBl19+CVmW0djYiFWrVsHpdGLv3r1Ys2YNiouL0b59+5Zjtm7dinXr1mHfvn14+umnAQAHDhzAqFGj0LNnT/zrX/864thTTz0VRUVFeOyxx5Cfn4+ioiKsXr0aRUVFGDx4MEpKSvDoo48iJyenpSYiIgIkVVVnAZjd1gMunrj35C86iQ+f6Rj1OY5WU1MDl8uFb775BqFQCIMHD475NQRz4zhda4mIkp3mRkSR+u677/D2228jMzMTDz74oOhyiIiojXQTRIMHD9bjKIiISPc0v1iBiIj0Lewg6lpsjeqC0R5PRET6EvbUHDe1IyKiWOLUHBERCcUgIiIioRhEREQkFIOIiIiEYhAREZFQYa+ae+qVVSgtr474ggV5OZj02xsiPp6IiPQl7BFRNCEUi+OJiEhfODVHRERCMYiIiEgozQXRkiVLcO2112LWrFnHbOnd0NCA22677YidUI9n+/btuP/++1u25j7a888/j5UrV7Z6fFlZGebOnYvFixdj8eLFmDp1Kv7973+3HHvFFVcgFAqdsAav14urr776hK8hIiINBtGYMWOQmZmJESNGoLi4+IjPpaen4/HHHz/hduEAcPrpp+PSSy+NuIbnnnsO559/PiZPnozJkydj1KhR2Lx5MwBg/PjxMJvNEZ+biIiOlFTbQCxbtgwrVqzA0qVLkZ2dja1bt+L5559Hhw4dkJGRgdWrV+Pyyy/HPffcAwBwu91YsGABduzYgbPOOgu33347tmzZgg0bNiAlJQVlZWUYNmwYunTpcsR1PB4PysvLW/6/Z8+e6Njx+Jv5vfLKK6irq4PT6UR9fT3uuusuOBwOvPrqq5BlGU8//TRyc3MxcuRIlJSUYMWKFcjOzkZZWRmuueYa9O7dO35/YERESSCpgujWW2/F2rVrAQCqqmL+/PmYPHky+vXrh61bt2LFihW44447Wl6/a9cuPPfccwiFQrjxxhtx8803o3fv3ujXrx+ys7MxYsSI415n1KhRmDNnDj755BMMGjQIgwcPPmZ0dkiXLl1a9kFatWoV3njjDdx0000YPXo01q5di7vvvrul3rlz5+Lhhx9Gx44dUV1djQkTJmDZsmWwWtmRnIiMK6mC6HAVFRUoLS1tGVF069YNFsuRX86hj1ksFuTk5KC2thYul+uk5+7Tpw+WL1+Or776Cp9//jkmTpyIkSNH4pZbbjnmtenp6ViwYAFcLhdKSkrQrl27456zqqoKu3fvxurVq1s+lpeXh6qqKhQUsKM5JZ6iAE1+BU0+FV6fgia/CkVRYbNKSLFJsNtMcNgk2G0SbNYTT4cTRSNpg+hk94kAwGaztfzebDafdJHDITt27MCpp56KQYMGYdCgQRg6dCimT59+TBC53W7MmDEDr7zyCnJycvDBBx9g/fr1rZ5XVVVMmDChpS6/339EjUSRqncrKCkPYm95EDUNCqpqZdS7FTR6FbibFDT5FPgCKnwBFUEZkGUFigqYTRIsZkAySTBJKqwWEwJBFaoKKIqK0MH/qipgNgEWiwk2C2CzSrBbJbTPMiMr3YxO+VYUtLMgP8eCvBwLcjN5H5XaLimCqLKyEitXrsTEiRNbPpabm4vCwkJs3rwZ/fv3x/bt2yHLcpvOZ7VaoSgKampqsGPHDpx99tlHfH7JkiW46667UFhYeMT1jub1eiHLcssoq6Ki4ohrHAq+Dz74AIMHD0bnzp2xfv16DBw4ELIsY9q0aVi4cGGbQpVIUYCS8iD2lAXx464Aft4fwL4qGdV1IagqYLVICIWAgKygjT9zQQ6pkEMAcOiA1leDyiFADinw+X/52P7K5n9zktR8fZMJUEJAQFaR7jQhJ9OCwnZmdC604dT8IM7qUA1nQbeIvn7SL80F0dKlS1FfX49Vq1a1LJn2+XxwuVxYtmwZ3G43lixZgnvvvRfTp0/Hs88+i3Xr1qFTp06w2+2wWCzYu3cvPvzwQ9TV1WHDhg0oLS1FdXU1li1bhgceeADnnHMOlixZgm3btuH6668/poZzzz0XTz31FIqKigA0T6tNnToVAPDSSy8hFArhxRdfxJ133ombbroJDz30ELp164aSkhLs378fX375JQYOHIhzzjkHixYtQigUwiWXXIKZM2fixRdfxPr16+H1ejF27FiYTJpbuEga8P1OP34+EMT2vQHs2B9AaZWMRo8Ci0WCHAJU5dikCcptTJ84UFUgEDzy+g1NChqaAth1APjPFi8cpiDG5f0vOph+gD2rEK6CM5DWZQBchWcgpV0nQZWTFkiqqs4CMLutB/zh8ReivuhjU8dFfQ4A2LBhA/r27QtJklBeXo7p06fj5Zdfjsm5NcgNIFV0ERQfP+zy46vvffhiixc/lQSQnWGG26MgIDBcYs0mBTA1fwoyzLW/fNBkgslkgaoqsGcVIbVDH6Sfchachd1gS88TVywllOZGROHYtWsX3n33XWRlZaGiogIPPvig6JKI2uSnkgA+3ejFl1u82HUgAKvFBF9Aafl8Tf2JH5hORgpMR4YQACgKFCUAAPBV7YGvag+qt7wLCSbAZEJ6lwFI69QPmaefB2vasdPjpA9JHUS/+c1vRJdA1CZ7SoP45gcvPt7gxba9AUjSkVNZocNCSK+yLW1reKzKgZY7VnVbP0HDjv9i3wfPwebMRGaPi5DV42I480+PX6GUcEkdRERa9tUPPny2sQkfftMEFYA/qEIJ6WeqLVwFtn0nf9FxKHLz6gh/QznKv1yBym9XA5IJ6Z0HoF3/q5He5eyTnIG0LuwgKsjLiXo/IiK92rkvgNUfN+LDb5pglgCPz7jBcziTpKKD9aeYnEsJNgdT3fZPUf/zl1BDIaR26IN2Z16JjNPPg9l+8mcFSVvCXqxAwnCxgkbVNYbw+r8b8fYXbni8gN8A02zhclgCuDXjaXR3bIrbNUwWO1Q1hNSOfZDb/3pkdjs/btei2OLUHFGE3vnSjdc+aMSeMhkhA0+5tYWiAO2t++N7jYNTeI271sOz/0fgTSC37zC0HzgS1tTjdzwhbWAQEYVh43YfVn3YiP9s9sJiliAzgNpEVszItlQl7HpKwAsAKP9qFSq+fQOO3M7IP28UMk/nKEmLGEREbfDXd+qx+t+N8AeBJl/z1BtDqO0yLbUnf1FcqFBDATSVbcfu1XMAsxXt+l+L/IE3weLMFFQTHY1BRNQKRQFe+GctXvvADauledUbRabAdkB0CVBCQSAUROVXf0flN6uQWtQDeb++FeldzhJdmuExiIiO4vWreOa1Wrz9uRsmSYKiqvAHRVeVvCRJRQdLbFbMxYKqKIASQOOejWgq+wnWtHYovGAsMrsNFl2aYYUdRP0AbIzign0BbIjieKJ4qWsMYfGKWvxnUxNCKgAVCLW1eyi1ym4KIs9WIrqM4wr5PQj5Pdiz9jHsf/8ZFF0+CZmn/Vp0WYYTdhBtjPKC0R5PFGvlNTKe/kctvvrex/s+8aCqyLOIn5o7kZC/CSF/E3b/czasziwUD5mMjK4DRZdlGJyaI8PaWxbEs6vqsGG775jO0RQ7fsWKXEuZ6DLaRAn64a8vw65VM2FNa4+OQ+9HWqd+osvSPQYRGU51fQhP/6MOX//ohdfHh0/jLdNSJ7qEsClyAP7afdj5jz/AnlWEjlfeD1dRT9Fl6ZYmg2j37t3429/+BrPZDJvNhoqKCvTp0wc33XQT9++hqDy7qhZvfOJGQFbbvHkcRSffViq6hIgpQT+8FT/jp+VT4MjtjI5D7oUj7zTRZemO5oLowIEDeOihh/DEE0+0bExXW1uL8ePH47rrroPD4RBcISWjz7d4Mf/PVQjIYjeQMxoTVBRraMVcpJRAEzz7f8COldOR1qkfOl89XXRJuqK5IPr73/+OX//61y0hBABZWVl44IEHUF5ejieeeAKqqmLy5MnIycnBo48+itzcXEybNg0zZ85EUVERAoEA0tPTcdttt8HtduPhhx9GQ0MDzj//fGzatAl2ux3z5s2D1WoV+JVSIjQ2KZj2p0rs3Bfgc0AC2Mwy8q3aXDEXiWBDJWq//xC1P3yIzldPR1aPi0WXpAuam+faunUrOnbseMzHzz77bHTu3Bm///3vIcsyunXrhnbt2qF9+/aYMmUKAODSSy/F+PHjcffdd2Pnzp3YunUrUlNT8eCDD6K0tBRDhgzB4sWLoaoqvv3220R/aZRgz62qxQ1T9+OHXX6GkCAqFORrfMVcuFRFhhqSseetJ7D1lTsR8jWKLinpaW5EdDI9e/aEyWTCpk2bkJ2djcLCQlgszV9GfX09nnzySTgcDpSWlmLfvn3o3r07ACAnJwcFBQUAgKKiIlRXR76VBWnb+q0+zHm5Cm6vCkVhAInkD9mRF+dmp6IoQR+aDmzF5sXXI//cW1AweIzokpKW5oKoW7duKCk5dihfUlKC3NxcpKSk4Nprr8Ubb7yB3NxcDB8+HADw1Vdf4Y033sDzzz8Pk8mERYsWQVF+WRFls9lafm8ymaDyTrXuBIIqZj5fic07/PAF+PerBRmWetElxJ2qyCj7fBmqNr2NU2+cz8UMEdDc1NzNN9+ML774AqWlv6y02bdvH2bNmgWz2QwAuOSSS7Bp0ya43W60a9fc3r2xsREul6tlVV1FRUXiiydhXvt3I4bdW4KvfvQxhDSkvTU5nh+KlqqEEGyswrZX78GetY+JLifpaG5EVFBQgAULFuDVV1+FyWSCzWaDx+M5YnGB3W7HoEGDcPHFv9woPO+88/DRRx9hzpw5yMvLQ0NDA95//31069YNy5cvR21tLdauXYsOHTpgy5Yt2LNnD3r16oXOnTsL+kopVu58rAz7K2WE+EiQpkhQ0cG6U3QZCaUEvaj57n3U/vhvdLlhHhuqtlHYO7RKMbhoND+vlpaWoqCgAPPnz8e0adNiUE3S4A6tR9n4kw/3L65o/n7iIEhz7OYghme+hH6OL0SXIoQ1NRuZp5+PDkPuFV2K5mluRHQyixYtQnp6Oi677DLRpZBAf1pZg7c/9/ChVA2ToCDPvE90GcIE3TWo2vIOGnavR49xr0AymUWXpFlJNyIyMI6IDrrtkVIcqOL23Fpngor5RWNglkKiSxFOkiR0HTEf6aeykerxaG6xAlFrvv7ei4sn7kVJeZAhlAScZjdD6CBVVfHz6lnY+6+FokvRpLCDqG+UF4z2eDKmp/5egxkvVIkug8LQ3louugRNUYJ+VG/6F75/fpToUjQn7CDagOaptUh/cVM8Ctfv5pbirc883KohmUhAB9vPoqvQHFUJwV9dgvWPXojG3ezucgin5kizNmzz4+KJe7GnNMgN65KM3SSjwLJbdBma9vOqmdj3wXOiy9CEpFs1R8bwt3fqsXxdg+gyKEImyMiz6LO1T6yE/B5UbfgnAjUl6DLij6LLEYojItKcBX+twYr3G+HxchSUrPyKFflWfTU7jQcl4EPdzv/iu6dHiC5FKAYRacqUpyvw4dceuJvYJiGZpZh8sEp+0WUkB0VGoLESG5+4AmooKLoaIRhEpBlj55Vi4zZu2aAH7S3s9RguRfZjwxNXwF9jvCnN8IOoXz9AkiL/1a9fHL4MSnY3/GE/dh0IIsRtG3ShyLZLdAnJSVXww0u/RcPPX4uuJKHCD6KNG6O7YrTHk64oCjBkcglqG/jgo17YTCEUWhhEkVLlIH5e9TCqN70lupSE4dQcCVNZI+PSu/YiwG0bdMUkycjnirmoKEE/St77P5R99oroUhKCQURCbNsTwP/MLj35CynpBBWzbndlTSQl6EXZF3/H3refFF1K3GnuOaIlS5ZgzZo16Nu3L7Kzs1FfXw9ZljF16lS4XK42n+fll1/GypUrsXr1ajgcjjhWTOH6z2Yv/vjnKgRljoT0yCYFkWLyii5DF5SgFzXfvwc1FESnq/8gupy40dyIaMyYMcjMzMSIESMwadIkzJw5EwCwevXqsM4zduzYlo30SDu+3erFY0ur4fMzhPQqx1opugRdUYJ+1P7wIfa+9YToUuJGcyOio6mqioaGBjQ2NrZ87NCOq5IkQZZlTJgwARaLBVu3bsXzzz+P/Px87ryqQf/Z5MVjr1bxQVWdK7LtEV2C7iihAGq+/wChoBenXDdLdDkxp7kR0SErV67EwoULccsttyAQCOC6664DAHz66afYuXMnJk6ciAkTJkCWZaxZswaqqmL+/PkYPXo0pk6dijPPPBM+n0/sF0Et/vu9F4+9Ws0Q0jmLKYRii7G2B08URfaj/qcvsPdt/W0lodkR0YgRI9CrVy+sXbsWTU1NKCgoAAB8/fXXKCsrw1NPPQUAqK+vR21tLSoqKlBaWorevXsDALp16waLRbNfnqHsKAngkZeqOB1nAFYEkcfWPnGjBH2o+W4dzHYXii6ZILqcmNH8O/WQIUMwevRonHvuuSguLgYA9OvXD2PHjgXQPHUXCARQX18vskxqRU1DCBOfKIPMx4QMQVbNbHYaZ4rsR+W3/4Q5JRX5g/Sxt5Fmp+YOsVgsuOqqq7B8+XIAwIABA7B+/XqoavNP1++88w7WrVuH3NxcFBYWYvPmzQCA7du3Q5ZlYXVT88OqI6btZwgZiElS4DI1nvyFFBVF9qPs87+ieqM+HnrV3Iho6dKlqK+vx6pVq2C1WtGtWzcMGzYMo0aNQkpKCsaNG4eSkhI8+uijyMnJAQCMGzcOkiRh+vTpePbZZ/HRRx8hMzMTdrsdL774Iu655x7BX5UxXXVfCVTOxhlKtoW76CaKEvSj5P3/g8WVhYzTzhVdTlQkVVVnAZjd9iOk6K/Kd6dIuAGkii6irUZM24/qeg6FjGZA6hcYmfms6DIMxWxz4NRbnoSrsIfoUiKm+ak5Sj7j5pcxhAzIYlJQbNkhugzDCQW8+Olv98Ffs090KRFjEFFMTftTBXYdMOaeKkZnlYJs7SOIEvDhp+UPQPYl5/05BhHFzKLlNdi43Y9QiFOvRhRSJeRZuHRblEB9GX54frToMiISfhD17RvdFaM9njTptQ8b8fH6Jm5qZ2CqKiHdXCe6DEMLeRuwY8UU0WWELfwg2rChebFBpL82bIjDl0Ei/bgrgFfW1qHRw+29jSzLWi26BMNTlRDcJZtR9tmroksJC6fmKGr3LCpDk48jIaMrsibvzXI9UYJ+lH6+DO6SzaJLaTMGEUXlzsfKEOICOcOzmBQUW38SXQYdpMp+bP/LJNFltBmDiCL2wupa/LyfK+QIsEoBtvbRHBXf/Wmk6CLahEFEEdmyw4/XP3JD5go5AhBSTdweXIMCDZXYvWau6DJOikFEEblvcTkCXCFHB4UUEzItXKygOWoI9T99jiqN96RjEFHYxs0vQ4gL5Ogw2bYa0SVQK0IBL/a9/zT8tdp9xotBRGFZ8kY99pQGRJdBGpPPB1k1TQn48PNr00WX0SoGEbXZtj0BrPqwAUHurkGHMUsqOnDFnOb5q/dh9xuPii7juBhE1Gb3PFkOb4D3hehIVlMQ+XyGSPMURUbd9s80+XwRg4ja5P6nKthDjo5LVVUu3U4SSqAJO/8xTXQZx2AQ0Um987kb2/cGoDCH6DgCqhU5lgrRZVAbhQJe7H5zvugyjsAgopNa+LdaeLxcJkfHl2WpFV0ChUNVUPfjR/Ac+EF0JS0YRHRC8/5cDYk76tIJ5Fu5Yi7ZKLIfO/7+B9FltGAQUat2lATwn01e8NYQtUaSgI5W7sqajBS/B/veXSy6DAAMIjqBh5+rhD/AKTlqnd0UQJ6lRHQZFAFVCaFq87vwVe0WXQqDiI7vb+82oLqebbXpZLhiLpkpQS92rHhQdBkMIjqWqgIvvVHHNj50Uv6QHe2tpaLLoCjITQ048PHLQmtgENExpj1TCfC+ELVBuqVOdAkUJUX2ofLb1yE31QmrgUFER/hupx+bf/KLLoOSRD5HQ7oQ8nux9+2Fwq7PIKIjPPJSFXxcoEBtIEkqOnDFnD6oChp2fQNf9V4hl2cQUYvl7zXA3cQQoraxm4LIt3LFnF4oQR/2COq4wCAiAIA/oGLJm3Xwc7M7aiNJVZBn5Yo5PfFW/IzGvYlvisogIgDA4hU1XJ9AYfEpduSbGUR6osh+7Fn7WMKvyyAiNHgUfPhNExQ+NkRhSDM3QJI4las3wcYq1G39KKHXZBARnnu9FiG21qYwtbeWiy6B4kANBVDyzlMJvSaDyOD8ARXv/dcDhT/YUhgkCSi27RRdBsVJKOBB1YY3EnY9BpHB/WlVLUOIwmY3BVFg2SO6DIoTRQ5g3/vPJux6DCIDU1Xgrc/cosugZKSGuP2D3qkhlH7+14RcikFkYH9ayZVyFJmAamOzU51T5ACqN6xNyLUYRAb2z4/d7ClHEXGYmmCRgqLLoDiTm2pR+e0/434dBpFBvbK2rvmOM1EE2lsrRJdACaAE/Sj7/C9xvw6DyKBWvNcIhUu2KULF1p9Fl0AJEvJ50LhnfVyvwSAyoNc+bITKOTmKkN0cRKFlt+gyKEGUoA8HPnoprtdgEBnQ0rfqEZRFV0HJyqSG2GPOYJrKfkKwsTJu52cQGcy7//VwgQJFJaBauWLOYNRQEPvjOCpiEBnMX/9VD4+PT7BS5GySH3aTT3QZlGA1362L27kZRAaytyyImgaGEEWnnS1+UzSkXZJkQsU3r8fl3AwiA1nziRs+P4OIotPBskt0CSSAqsio+OJvcTk3g8hA3vvSDZX3hygKNrOMQiuDyKhCfg8a92yM+XkZRAaxYZsPQe43RFEyQ+aKOQMLBbwo++yVmJ+XQWQQy99rQIDbgFOUgooZ+RY2OzUyb+VuyN6GmJ6TQWQQ327lKieKnllS4DCxY7uRhfxNqP3xw5iek0FkAP/63A2JfeUoBtpZuWLO6NRQAFUb3ozpORlEBvD6R+wrR7FRZOVmeAT4Kvcg5PfE7HwMIp2rawxhbxn7+VD0bOYQiizcHpyam7PUfvdezM7HINK5t/7j5lbgFBMmyMjnijkCAEVGZQyn5xhEOvfmp26EOC1HMRBSTewxRy28lbuhBGOzCIpBpGM79wXg9jGEKDYkVUWqObbLdil5SZBQ831s+s8xiHTsP5u98PsZRBQb2dZq0SWQhqhqCNUb1sbkXAwiHVv3Xw+n5ShmCq17RZdAGuMp+wlqKBj1eRhEOuX1qyiv4Wo5ig2LSUEH6w7RZZDGmMxW1P747+jPE4NaSIM2bPPBbOZDrBQbVimIPLb2oaMosh/VG9+K+jwMIp36ZH0T/AFOy1FshBSgPZdu03H468sBNbqOygwinfpmm1d0CaQjCkzINNeILoM0SPY1wF3yXVTnYBDpUINHQYObT7FS7GRZGUJ0fGrAh8Y9G6I6B4NIh77+wYsQc4hiqNC6T3QJpFGqqqBhx5dRnYNBpEMfrW/iTqwUM2aTgmIzV8xR65oqoutByCDSoe92BkSXQDpik4LIt3FERCdgkuCr3B354bGrhLSguj6EJh/n5Sh2FAVob+XSbWqdKgfRsOubiI9nEOnMeu7ESjEWhAU5Zm6IRyegqqjf8UXEhzOIdOazTV4EZd4gotjJttSKLoGSgLfsp4iPZRDpzKafOCKi2Mq3cVqOTk4J+hD0RLbMn0GkIxW1Mhx2/pVS7JhMKootkf+kS8ahmgBPhA+28l1LR/aWyWhsiq7VBtHh7FIA+RaumKOTU4MBNOz6OqJjGUQ6srcsCB9XblMMKaqKPK6YozZy79kY0XEMIh35cXcACvcfohgKKHbkWkpFl0FJIhRsAhD+exCDSEd2HeBwiGIrkyvmKAyKrwn+uvB/cGEQ6Uh5NTfCo9jKs3E0RGGQJPhrw98uhEGkE/6gCq+f03IUOyYJ6GBhjzlqOyXog6+GQWRYJeVB7shKMWUzB1BgLRFdBiURVVXgi+DBVgaRTuwpkyGHOCKiGFIU7spKYfNW7gr7GAaRTmzb7efWDxRTfiUFeWYu3abwBBrKwj6GQaQTPx8Iii6BdCbNUg9J4k83FB65qSHsYxhEOrG/givmKLbybOH/ZEskSSYE3dVhHcMg0onqegYRxY7EFXMUKZMJ/trwpnQZRDpQXR+C2cQVcxQ7dnMQBZa9osugJKSGQvDXhbfIhUGkAyXlMqwWBhHFjqSEuGKOIqIqQfir9oR1DINIB6rrZYS4OzjFkE+1I9/CFXMUGW/l7rBezyDSgSafiiCfIaIYcpncMEu870iRUeXw+l4yiHTA4w1B5vbgFEPtbeWiS6Ak5qsJ7/4ig0gHahsZQhRDEtDB8rPoKiiJhYL+sF7PINKBejd3ZaXYSTEFUWAJ72Yz0eE4NWdAjU1cqUCxFEIeV8xRFNRQeJ1eGEQ60MAgohgKhqwMIoqKqiiA2vb3JQaRDjR5eY+IYsdu8sEmhTfHT3Q4yWRCKOhr8+sZRDrgD3BERLGTa6sQXQIlO8kEJeBt88sZRDoQ4OMeFEPF1vD3kyE6nCRJUDgiMhZ/kCMiig27WUahhUFEUeKIyHiCQd4jotgwQUaela19KDoSwHtERsP2PhQrwZCFK+YoaqoEjoiMJKQACmfmKEYspiAcUpPoMijZqRKUYNuDyBLHUigBmnwKrGYJAfaaoyhIkorMlApcu/FvKNjKrgoUHcVhgzKEQWQYqormCVmiMDmsbmQ5KpCTVoYUSwN2n1KAkf/3DGyB8NqzEB1NtVogDS8Derft9QyiJGe3SggpHA3RyUmSigxHJdqnliHNXgnJpMIsKVAPPgFfnp+Pjy+8EJe9957gSinZSTY74HS2+fUMoiRns0q8R0Stclg9yHBWoL2redQDyQwJMg796KIe9jNMY0YaHp47FwP/+1+k1dcLqZd0xOFo80sZRElOkpofHlNVjooIkKAgw1GFdmnlyLBXQJJUmE0qVPVQh/ZfQuhoDRlp2NG5O/Z16IAzGEQULQaRsVjMALv8GJfD6kGmsxLtXGVIsdZDggUSgscd9ZyI29X8xjFt3jwsu+02uBhGFA1OzRmL1SwhwIdajUNSkemoQq6rDGkplTCZlCPu9eCwEAqHx5ECAFhz7bXwpKQwiChyqsoRkdFYLVw2p3cpVg8yHZXITSuDw1wHSBYAvzQZjMXMrC/F1vL7uTNnYuGDD8Lu8UR/YjImjoiMxWoVXQHFXvOop11qOdJTKmGSQsfc64klxWSCbDa3/P//TZyIJ++9N6bXIAPhiMh4bBwR6UKKpfleT25qGRzWOqiwwhTBvZ5IeFwOWOQQgtZf3hKeuu8+3Pv//h8sfu5NRGFSFI6IjMZmZRAlq0xH5cFRT8XBez2/jHqkCO/1RMLjcsCsqjh8g+e506dj0qJFCaqAdCUU4ojIaIrbW7HrQHh7xJMYKZYmZDor0C61DE5rHVTJCkmVgYORI2oVvsflPKZBR2NaGv45fDhGrFgBiQ+rUThkmUFkNIdN7ZMGZRy815ORUn7wXg9+GfWo2vgBwuNyQDEf2wP54dmzMfTNN5Ha2CigKkpaGRnNDzm2EYNIB4pyrZAkcT9N05Hs1iZkOSrRLrW0edQDK6TDJr20+PfU5HQgaDo2iH467TRs7tcP537yiYCqKGllZob1cgaRDrTPMsNmleAPaPAdziAyHFVo5ypHhqMCZikEyaQCh93r0brGzHQorfwEO23uXKy99lqk1dUltihKXkVFYb2cQaQD7TLNsJgl+BN2a5vsliZkOquQm1YGp7kGqmSBhBAO3etJtr+KxvTUVj/3yeDBqMjNZRBR23XtGtbLGUQ60C7TzJ0gEiAjpQo5qb+MesySChWHRj2xfa4n0RpTT7zU9uE5c/Dy+PFwNjQkqCJKWiYTcPrpYR3CINKBdhlmbhceB3aLF5muSuS6yuC01hx8rkeGemiFm+D6YqnpYHuf1qwYORLPTpyItj8ZQoblcAAdO4Z1CLcK14GsdDN7zcVIuqMKXdr9gP7FH6FP4WfonLUNTmsNgEPP9ejzz9lrt530NY8+9BACYTykSAZlsQDFxeEdEqdSKMHSXGY0uEMnfyEdwW7xNvdwSy2F01oLVTpq1KPP3DmCYjJBtpz8GYAF99+Pxx58MAEVUVJTFKBDh7AOYRDpRFaaiUHURhkp1chJrUCGoxxmST6ih1siuxloxfHa+7Tmubvuwvhnn4U5qP2VgCSI18sgMqpunWzYU8o3h+OxWXzITKlA+7QyOGy1ACyQJLlluGOEUc+JNDmPbe/TmlkzZmDMCy8wiKh1djuQcuJ7jkdjEOlEXpYFJhO4bfhB6QdHPZkpZbCYZEhmCVAOrWwL6mulQZQ8LkebH4KvatcO7w0diqtXr4Zk9ASn4wtz6TbAINKNLsVWOG0muH3GTCKbxXfwXk8ZXNbm53oOv9cDY/6xtInH5YBynK4KrXl49mxc9P77SONSbjqeMKflAAaRbnQtskEx2I/56Sk1yHGVI9NRDrNJhumobgbG+tOInMflPG57n9Zs6d0b27t3x6+++iqOVVFSsliA888P/7A4lEICFLe3wBcQXUV82c1eZDiauxkcGvUcfq+HyRMZd0Zaq+19WjNt3jy8PmIEUrmdOB3O6QT69w/7MAaRjhTkmLG/Mrmf8D9aWkoNclwVyHKWw2wKwiQBUJu/Ron3emKiIaP19j6tWXfZZajNyGAQ0ZECAaBv37APYxDpyGkdbEkfRDaz7+CopxQuWw2gWmCSDrvXw+CJObcrsodUZ86Zg2fuvhsObhFBh9jtQG5u2Iexs4KOnHGKHW14FERz0lNq0Dl7K/oVf4w+xZ+iS7utSLVVQ4IKSdJvNwOt8JykvU9rXrntNoS4co4O16NHRIcl4dsWtaZLoRV2mwlBWdtLxKxmf/N+PQfv9UAy87kegXwp9oiPXTh1Kh764x9h9fliWBElJUkCBg2K6FAGkY6cUmSFLGvzXTzNXoOc1ApkOcphNgVgMkkt93oAhVNugqiShEAUw+h506Zh1syZMayIkpbLBZx9dkSHMoh0JDvdjLxsM/aUib9PZDP7j7zXA/ORz/UweDTB43LAKssIWiJ7KwiZzfjzHXfgt0uWwCSL/74jwSJYqAAwiHSnXaZFWBClptQix1mOLGcFLCY/zCYJ6mGjHmaP9nhcTpiVtrX3ac2sGTMwctkyOBlExub3A6edFtGhDCKdObePA9/t9MOfgG0hrGY/MhyVaJ9aDpetCoAZJpMCVW2+R8V7PdoXTnuf1uwrLsZ/Bg/GZe+8w790I7vooogPZRDpzJmnp8BiBvxx6kmZZq9FlqsCOc5yWEx+mCQJasvupDLfh5JMk9MBRYp+8ez0OXMw8PPPkcbniozJbAZ+9auID2cQ6UyXQitiuWjOag4gM6US7dLL4LJVQzrqXg9zJ7l5XA7I5uiD6JsBA7C3Uyf03Lw5BlVR0klNBa68MuLDGUQ61LOLHeu3Rr6cNs1ei+yD3QwsJv8xK9wYPvrRmJGGULRzcwdNmzcPy0eNgoujIuNxuyPqMXcIg0iHzu3dfJ+orduHW8yB5s7VaWVItVZDlcwwSSHg4L0eJo9+NWakxexcb159NdxOJ4PIiCJctn0Ig0iH+nZLgdUMBE5wnyjVXodsVwWynWWwHnWvR+JzPYYRaXuf1syZORNPTpmCFLc7puclDbPZgN/8JqpTMIh06Hj3iaymADIclchNK4fLXg2oJphNMlSV93qMzOOMrL1Pa565804smjw5puckjUtJAS67LKpTsNecTvU8xQ6XvQ4ds35C36JPcWbxR+iS+yPS7BUwIQSTFGwJITIur90W83Muvv9+yPbI2wZRkgmFgDPPjOoUDCKduv58H3rkfYuCjF2wWTwwScphCw6ImgVs1pifc+706VC5Z71xDB4c9SkYRDrVp1s72Kxqy8OlREfzuJywxqEbgsflwmsjR0I1m2N+btIYpzPq+0MAg0i30lxOZGWmiy6DNMzjcsCsxGd6dsasWWhyOOJybtKQzEzg0kujPg2DSMfO6nM6rPyplFrhcTkQmyeIjrWza1dsGDAgTmcnzXA4gM6doz4Ng0jH+nTvCskUr7caSnYelxOqKX5vAdPmzkVjZmbczk+C2e3A7bfH5FQMIh3LSHMhIy1VdBmkUR6XA8EYtPdpzWfnnYeyvLy4nZ8Es1iAW2+NyakYRDr3q97dYInjmw0lL3d6asza+7Tmoblz0ZTOe5W61KkT0KFDTE7FdyidO/OMLjCZeJ+IjhXL9j6tWTliBHwRbrpHGuZwAOPGxex0DCKdy8pIQ1oqVy/RsdypsW3v05o/zpiBgDMx16IEuuWWmJ2KQWQAv+p1OsycnqOjuJ2J+QFl0eTJMPv9CbkWJcgZZwC5uTE7Hd+dDODMHqfCwukROko82vu05pm770bIlrjrURy5XMCdd8b0lAwiA8jJTEdGqkt0GaQx8Wjv05rZM2ZAjuNScUqgQAC4+eaYnpLfGQZx4bn9YE/gGw9pm8fliEt7n9bUZGfjnWHDoMZ5lR4lwNChzTuyxhCDyCD69zgVITaipIOanPFr79Oahx55BJ60+K/UozjKyIjZQ6yHYxAZyMC+PWDm9AihuatCoscm3/fsiR979EjwVSmmbDbgqqtiflq+KxnIeWf1hpm95wjNU3PxbO/Tmmnz5sHNtj/JyeEApkyJy6kZRAaSmZ6Kwrwc0WWQBnhcDsgClvR/cMklqGEQJSe/H7j33ricmkFkMBcN7IuUFO6eaXTu9FTIghYOzJgzB17eK0ouJlNzX7k4PQbCIDKYbl07wsyFS4aXiPY+rXl11ChhIUgRcrmAadPidnoGkQGdd1YfWPmAq6E1Cn6ubMHUqQhw47zkcfrpzd0U4oRBZECDftULCrcQNzSPM0Xo9edNmwab1yu0Bmqj9HRg5sy4XoJBZEA2mxW/6n06TFzKbVhewfcJVUnCS3feCYUjc+2zWIBrronrJfhOZFCXDhrAfYoMzK+BLhuzZsyAj/3ntC01FZgzJ+6X4TuRQaWnOtHz9FM4KjKgJqcDFjkkugwcKCzEpxddBHDhgnYpCvD738f9MnwXMrBhFw3k9hAG5HE5YNFIu6fpc+agkTu4apPLBcyfn5BL8V3IwFJdDvTp1gUmhpGheFyOhLf3ac36/v2x+5RTRJdBx2OxAPfck5BL8R3I4IZePBBmTo0YSpPTAdWknb/zafPmwZ2RIboMOlxqKrBgQcIuxyAyOJcjBWf2OI3NUA3E43JC1lDPwbeGDUOji/tlaYrDAdxxR8Iux3cfwrCLzoGkoZ+QKb5EtvdpzSOzZsEX4z1uKEJpacDixQm9JIOI4Eixo3+v09iZ2yAaM7XX5+35ceOABG7URyeQkQHccktCL8kgIgDAsIt+zW8Gg2hMdYou4bgWTZkCOUVsxwfDS08H/vd/E35ZvvcQAMBus2LQgN6wWvmku965ndrs8TZv2jSOikTr0we4/vqEX5ZBRC2GXHg2LFy0oHteuza7GXgdDvz91luhcopYjIwMYNEiIZfmuw4d4aarL+Z+RTrn12gQAcCMWbPQxK7ciWexAEOGAGedJeTyDCI6QveuHVHYPgdcRKdPXkeKJtr7tGbXKafgm7PPFl2G8cgysGKFsMsziOgYt157KUycHtElj8sBs0ba+7Rm+ty5aOQDromTlgY8+6zQEhhEdAyXMwWDzz4TNqv4Ds0UWx6XU/P/6D8/91yUFhaKLsM4CguBO+8UWoLWvydJkMvPHwCrhaMivfG4tNXepzXT586Fh6Oi+EtPB157TXQVDCJq3U3XXAyHnQsX9MTjckA2af8HjFU33AAfR+TxZbMBN9wA9OoluhIGEbXu9M7FKC7MhUkzvZopWp60VMhJMCICgHkzZsDPHnTxo6rAkiWiqwDAIKKT+J/rL0N6Ot8M9KJBg+19WrP4nntg9vtFl6FPubnAsmWiq2jBIKITslutuPKCc5CSot1nT6jt3KnJ9UPFnyZNQojbiceW1QoMGACMGCG6khYMIjqpM3t0RdcOhWyKqgNuZ3L1cpv98MOa6xSe9Mxm4O23RVdxBAYRtcmo31wOs5lvCMnOm2RdM+oyM/HWtddCZeup2MjMBNatE13FMfi3S2027uarkaLh9jB0cn5b8q1Ee3j2bHi4V1H0nE5g9GjgvPNEV3IMBhG1WXF+Ls7pewZsNnboTkZeRwrMIW13VTieH884A99rYIlx0svLA556SnQVx8UgorBceeE5yOBPp0kpGdr7tGb63LlozMwUXUbycjqBzz4TXUWrGEQUtgn/cw0sFo6Kkk2T05G0/+A/vPhiVGdniy4jOWVkAAsWNLfy0ahk/b4kgZyOFFx72SDeL0oyydLepzUPz5mDprTkeQ5KE8xmoH9/YOJE0ZWcEIOIInJWn27ocXpnjoySiMflhJzEq8+W3Xorl3KHy+EAPvxQdBUnlbzflSTcjUMvREaaCxLfHJKCJ82V1EEEAI9Pn44gN85rG6cT+Okn0VW0SXJ/V5JwU8bdBOZQcmjITBddQtQenToVVq9XdBnal5XV3FU7P190JW3CIKKo3X/HTbCzU7LmNaY5RZcQEy9MnAiF32+tS08HJk8GrrxSdCVtxiCiqOVkpmP40AvgSLKn9o3Go5MprVkzZsDPIDo+ux244AJg5kzRlYSFQUQx0bt7F5zd9wzYk/DJfaNo0knj2rL8fHx0ySXgnPBRJKl5ifYbb4iuJGwMIoqZKy84G52K8tgcVaP8OlpuP33OHDSmJ/89r5iSJODnn0VXEREGEcXUmBuHItWljykgPfGl2JO2q8LxbOzbFz937Sq6DO3IyAC++050FRFjEFHMTZtwCzLT+eChlnhcjqTsM3ci0+bNgzsjQ3QZ4mVlAX/5C3DGGaIriRiDiOLi3rHDYTbz20srPC6n7v6x/+vKK9Fg9E4LWVnNjUyvvlp0JVHR2/cmaYTdZsXUCbfAys4LmpDs7X1aM3vWLPiM2oQ3Kwt45BFg1CjRlUSNQURxk+5yYvKYG2DjSjrhmpwOhJK8q8LxvHj77VB1dO+rzTIymp8Vuvtu0ZXEhP6+M0lTcrIycOet17BBqmDutFQEdRhEAPDklCmQU5JrC/SopKcDY8Yk3bNCJ6LP70zSlML2Ofjt8CFw6OQ5lmTUkKXfeynzpk2DFAyKLiMxUlOBG24AFi0SXUlMMYgoIToX52PkNZfAye4LQrhTXaJLiBu/3Y6/jR4NRe/PrzkcwOWXA0uWiK4k5hhElDDdTumA64ecD6eDYZRobqe+p65mzJoFr05aGB2X3d58P2jVKtGVxAWDiBKqd7cuGHvjUDjsDKNE8up8JLqnUyd8PXCg6DLiIzUVGDYMePxx0ZXEDYOIEq4oPxc3XX0RUnjPKGF8BlgsMm3uXDTq7QHXjAzgf/5HtyOhQxhEJET3rh0x9sZhsFr5nFG8+e02mAywxPnLgQOxv7hYdBmxk5UF3Hcf8OyzoiuJOwYRCdOhIBcPjhspugzdc6c6YdFZe5/WTJ87Fx49jIqys4H583W1RPtEGEQkVFqqE49NHYeUFDtMbOsfF01Oh2H+oa++/np4bUk+DZmZCfz5z8D48aIrSRijfH+Sxs2edBuystJhYX+6mNNre5/WzJ05E35Xki5XT00F3n0XuOYa0ZUkFP/Vk2ZMueMmFBfmwcb7RjHlcTl12d6nNf97110wBQKiywhf587NWzmcfbboShLOON+dlBTuvOVqnHFqJ6TofLlxInnSnLpt79Oap++9F3KyPCJgtwPFxcCPPwKdOomuRghjfXdSUrj5mktw8cC+DKMYacww3k6mjzz0EBRVFV3GyWVkAFdcAZSUAEbql3cUBhFp0uBzzsTvbrgCACDBOPc34qExLUnvl0ShIT0db/zmN1C1PBLMzASmTQPWrBFdiXAa/lsio+tUnI/Hpo5DbrtM3jeKgtugW7c/PHs2PFpdtGA2A2++CUydKroSTWAQkebdN3YE+vY8DQ5O1UWkyaB/btu6dcOWvn1Fl3Ekux3o2BFoagLOO090NZrBIKKk8Jsrzsf1V5zPTgwR8Cf7czVRmD5nDhozM0WX0Sw7Gxg6FNizBzDw38nxMIgoafTp3gUPjhsJlzMFFovOW/7HiN9ug6Qao6vC8Xx04YWobNdOdBmAy9XcKeH110VXokkMIkoqaalOzLh7NHqe2okb7bWBx+UwTHuf1jw8Zw6a0gWtHHS5gNNOA7ZtA8aNE1NDEmAQUVK6+dpLcc1l50EC2BroBIzU3qc1y2++WcxzVOnpwKRJwPbtQFFR4q+fRIz+PUpJrF+PU/Ho1HHo2rmIo6NWuFOdAIMa86dPR8DpTMzFHA4gLw/4/HPgj39MzDWTHIOIkpoEYOyNQ/GbKwY3j44M1FOtLZqcDoTYvw+PT5kCq9cb/wtlZgKjRwNlZUDPnvG/nk7wO5R0oXf3Lpg/dRxOP6UDHAbYBK6t3Gkuw7X3ac1zv/89FKs1Pie32ZrvB735JvDcc/G5ho7xO5R05bfDh2D4sAthMkkw8w0YjZlpSIJGNwkxe8YM+C1xWP6flgaMGQO43Xw2KEL8l0q60/O0znh0yh3oeXpnpBh8dORO1WhnAQEq2rfHh1dcATVW98wyMoC+fYGvvjLELqrxxCAi3brl2kvxuxFXIjc707CBZNT2Pq15aPZsuKNdyp2SAjidzVNwGzYA3bvHpjgDYxCRrnUqysP9d9yI6y4/Dyk2m+F61hm1vU9rNp15JnacdlpkB0tS832gCRMAjwcYyW3uY4VBRIbQt8epmH3vb3HBwL6QTBJMBllJ5jPoSPBEps2bB3e4bX8yM5vv/2zbBixaFI+yDM0Y/xqJDrrk3P6Yd99Y9OveFXZbnFZQaURzex/RVWjPu1dcgbq2Ts9lZABdugDLlgGffMIHU+OEQUSGYzabMOKqi3DPb2/AKcX5SEtL0IOOCdbkdMASCokuQ5NmzZ4Nb1pa6y/IyGjukv3ii8DOnc3NSiluJFVVZwGYLboQOik3gFTRRejRrv3l+NeHX6Ciug5+f0A3y533Fefj9RFXwqvzkV+k3KmpcHk8R34wIwPIygIWLgRuuEFMYQbEEREZ3ilFeZg46jqMv+VqdOvaETarBZIOOjR4XI7YLVXWoScffBDBQ9tzZ2QAp5wCvPIKsGsXQyjBOCJKHhwRJUh9owfrPvka63/YAZMkQU7S6a0N/Xrg35eey84KrTCHQtjTpQuKbDbgySeBa64RXZJhGWstK1EbZKS5MHzYhbj28vPw/uff4tP/boHFbEJAlkWXFhZ3mgsyQ+gY5oO/BpnN8Hz6afO9IBKKQUTUCqvVgisvOAdXXnAOPv16M9Z/9xNq6xvh8wdEl9YmjZnpurnfFQt2AGkAfg9gEoAsgCGkEQwiojY4/6w+OP+sPtixez8+/3YLftxZAovFjGBQu6OkxjS295EO/hqM5vsPFwithlrDICIKw6mdi3Bq5yIoioJvNm/DZ99+j7qGRshyCIqirZ1Q3U7jtvdxAMgGMBHAAwD4WK+2MYiIImAymXB23zNwdt8zUF3bgP9u/AFfbd4GqKpmpu6aHMZq7+ME4AMwHMBUAP3FlkNhYBARRSknKx1DLxqIoRcNxI7d+/HNlq3YsnUXrFYL/IEgVFXMnRq/Adr7OAEEAVwM4GYAt4kthyLEICKKoUNTdyOvBn7eewDfbd+F77bvhs8fgKooCMqJWQoe0PFDrClonnq7FM3hczkA3g1Lbgwiojjp0rEQXToW4ppLB6G2vhE//lyCTT/swJ59ZUhJscHvi18XB4/LAYscQsiW/Mu3rQAUAL0B3ITmqbdThVZEscYgIkqArIw0nNuvB87t1wMAsG3nXmzZvgulFTUor6qBxWyGPxiEqsQmmjwuZ9K2TXEAkAEUAbgSzaOe84VWRPHGICISoFvXjujWtfkZFk+TDyWlFdh7oAI79x5AaUUVoEqQTEDAH4xo1NTkdDTvn6NxVjQ/3+MD0AdAVwAj0DzdliGwLkosBhGRYC5nCrp37YjuXX95uLK2vhH7Siuxa38Zdu0tRUV1LVKdDvj8AQSDMpSTLIDwuBwIaWjPJQnNbzYymh8k7YrmVW3nAuhx8Jc+e6BTWzCIiDQoKyMNWRlp6N29S8vHKqvrUFVbj+q6BpRV1qCyug61DW54mnyQVBUmswmqqkKWQ3CnuRLeY86O5oUEMoAmAAVoDpy+AAaiecRzBprb6xAdjkFElCRyczKRm5N53M81+fyorWtEbX0jqusa0NHbBKuiwGcyoRGAB83h4AMQQPOSZ6D5DeBQ7zUTmsMkAEA9+Es5+Ct08L8ZBz+fieYHRvMAFAIoPvj7TgC6A4hwM24yKHbfTh7svk0xFUBzQLkP+28Iv9y3Od6vFCGVkt5xRERkULaDv7JEF0KGp527mUREZEgMIiIiEopBREREQjGIiIhIKAYREREJxSAiIiKhGERERCQUg4iIiIRiEBERkVAMIiIiEopBREREQjGIiIhIKAYREREJxSAiIiKhGERERCQUg4iIiIRiEBERkVAMIiIiEopBREREQjGIiIhIKAYREREJxSAiIiKhGERERCQUg4iIiIRiEBERkVAMIiIiEopBREREQjGIiIhIKAYREREJxSAiIiKhGERERCQUg4iIiIRiECUPSXQBREQx4D76Awyi5OESXQARUQykHv0BBhEREQnFICIiIqEYREREJBSDiIiIhPr/83rkBBzxrNUAAAAASUVORK5CYII=", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Assistant] Looking at this pie chart, I can clearly see the different segments and their relative sizes. Let me crop the area showing the Cyan segment to examine it more closely.\n", + "[Tool] mcp__crop__crop_image({'image_path': 'chart.png', 'x1': 0.4, 'y1': 0.6, 'x2': 0.7, 'y2': 0.9})\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Assistant] Now I can clearly analyze the chart. Looking at the pie chart:\n", + "\n", + "**Yes, Cyan is the minimum.**\n", + "\n", + "The pie chart shows 5 categories with the following relative sizes (from largest to smallest):\n", + "1. **Royal Blue** - the largest segment (takes up roughly half the pie)\n", + "2. **Peru** (tan/brown) - second largest\n", + "3. **Red** - medium-sized segment\n", + "4. **Light Slate** (gray) - smaller segment\n", + "5. **Cyan** - the smallest segment\n", + "\n", + "The Cyan segment is clearly the thinnest slice of the pie, making it the minimum value among all the categories shown.\n" + ] + } + ], + "source": [ + "import tempfile\n", + "\n", + "async def ask_with_agent_sdk(image: PILImage.Image, question: str):\n", + " \"\"\"Ask a question using the Claude Agent SDK with file-based image access.\"\"\"\n", + " global tool_working_dir\n", + "\n", + " with tempfile.TemporaryDirectory() as tmpdir:\n", + " image_path = f\"{tmpdir}/chart.png\"\n", + " image.save(image_path)\n", + " tool_working_dir = tmpdir\n", + "\n", + " options = ClaudeAgentOptions(\n", + " mcp_servers={\"crop\": crop_server},\n", + " allowed_tools=[\"Read\", \"mcp__crop__crop_image\"],\n", + " cwd=tmpdir,\n", + " )\n", + "\n", + " prompt = f\"\"\"Answer the following question about chart.png. Use your crop tool to examine specific regions of the image.\n", + "\n", + "The question is: {question}\"\"\"\n", + "\n", + " async with ClaudeSDKClient(options=options) as client:\n", + " await client.query(prompt)\n", + "\n", + " async for message in client.receive_response():\n", + " msg_type = type(message).__name__\n", + " if msg_type in (\"SystemMessage\", \"ResultMessage\"):\n", + " continue\n", + "\n", + " if hasattr(message, \"content\") and isinstance(message.content, list):\n", + " for block in message.content:\n", + " if hasattr(block, \"text\"):\n", + " print(f\"[Assistant] {block.text}\")\n", + " elif hasattr(block, \"name\"):\n", + " print(f\"[Tool] {block.name}({block.input})\")\n", + " elif hasattr(block, \"content\") and isinstance(block.content, list):\n", + " for item in block.content:\n", + " if isinstance(item, dict) and item.get(\"type\") == \"image\":\n", + " img_data = item.get(\"data\") or item.get(\"source\", {}).get(\"data\")\n", + " if img_data:\n", + " display(Image(data=base64.b64decode(img_data)))\n", + "\n", + "\n", + "# Run the same question with the Agent SDK\n", + "print(f\"Question: {question}\\n\")\n", + "await ask_with_agent_sdk(chart_image, question)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/pyproject.toml b/pyproject.toml index 00023d8d..ff52ced7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,11 +9,11 @@ dependencies = [ "numpy>=2.3.4", "pandas>=2.3.3", "jupyter>=1.1.1", + "rich>=14.2.0", + "python-dotenv>=1.2.1", "voyageai>=0.3.5", ] -[tool.uv.sources] - [build-system] requires = ["hatchling"] build-backend = "hatchling.build" @@ -41,7 +41,11 @@ indent-style = "space" line-ending = "auto" [tool.ruff.lint] +select = ["E", "F", "I", "W", "UP", "S", "B"] ignore = [ + "E501", # line too long + "S101", # assert used (ok in tests) + "S311", # pseudo-random generators ok for non-crypto "N806", # variable in function should be lowercase (allow for API responses) ] diff --git a/tool_use/__init__.py b/tool_use/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tool_use/automatic-context-compaction.ipynb b/tool_use/automatic-context-compaction.ipynb new file mode 100644 index 00000000..7475f790 --- /dev/null +++ b/tool_use/automatic-context-compaction.ipynb @@ -0,0 +1,1242 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "eb718d24", + "metadata": {}, + "source": [ + "# Automatic Context Compaction\n", + "\n", + "Long-running agentic tasks can often exceed context limits. Tool heavy workflows or long conversations quickly consume the token context window. In [Effective Context Engineering for AI Agents](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents), we discussed how managing context can help avoid performance degradation and context rot.\n", + "\n", + "The Claude Agent Python SDK can help manage this context by automatically compressing conversation history when token usage exceeds a configurable threshold, allowing tasks to continue beyond the typical 200k token context limit.\n", + "\n", + "In this cookbook, we'll demonstrate context compaction through an **agentic customer service workflow**. Imagine you've built an AI customer service agent tasked with processing a queue of support tickets. For each ticket, you must classify the issue, search the knowledge base, set priority, route to the appropriate team, draft a response, and mark it complete. As you process ticket after ticket, the conversation history fills with classifications, knowledge base searches, and drafted responses—quickly consuming thousands of tokens.\n", + "\n", + "## What is Context Compaction?\n", + "\n", + "When building agentic workflows with tool use, conversations can grow very large as the agent iterates on complex tasks. The `compaction_control` parameter provides automatic context management by:\n", + "\n", + "1. Monitoring token usage per turn in the conversation\n", + "2. When a threshold is exceeded, injecting a summary prompt as a user turn\n", + "3. Having the model generate a summary wrapped in `` tags. These tags aren't parsed, but are there to help guide the model.\n", + "4. Clearing the conversation history and resuming with only the summary\n", + "5. Continuing the task with the compressed context\n", + "\n", + "## By the end of this cookbook, you'll be able to:\n", + " \n", + " - Understand how to effectively manage context limits in iterative workflows\n", + " - Write agents that leverage automatic context compaction\n", + " - Design workflows that maintain focus across multiple iterations\n", + "\n", + "## Prerequisites\n", + "\n", + "Before following this guide, ensure you have:\n", + "\n", + "**Required Knowledge**\n", + "\n", + "- Basic understanding of agentic patterns and tool calling\n", + "\n", + "**Required Tools**\n", + "\n", + "- Python 3.11 or higher\n", + "- Anthropic API key\n", + "- Anthropic SDK >= 0.74.1" + ] + }, + { + "cell_type": "markdown", + "id": "004e3b52", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First, install the required dependencies:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "71d7f8c2", + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install -qU anthropic python-dotenv" + ] + }, + { + "cell_type": "markdown", + "id": "1ba96da5", + "metadata": {}, + "source": [ + "Note: Ensure your .env file contains:\n", + "\n", + "`ANTHROPIC_API_KEY=your_key_here`\n", + "\n", + "Load your environment variables and configure the client. We also load a helper utility to visualize Claude message responses.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "68f6d5bb", + "metadata": {}, + "outputs": [], + "source": [ + "from dotenv import load_dotenv\n", + "\n", + "load_dotenv()\n", + "\n", + "MODEL = \"claude-sonnet-4-5\"" + ] + }, + { + "cell_type": "markdown", + "id": "783cf8e3", + "metadata": {}, + "source": [ + "## Setting the Stage\n", + "\n", + "In [utils/customer_service_tools.py](utils/customer_service_tools.py), we've defined several functions for processing customer support tickets:\n", + "\n", + "- `get_next_ticket()` - Retrieves the next unprocessed ticket from the queue\n", + "- `classify_ticket(ticket_id, category)` - Categorizes issues as billing, technical, account, product, or shipping\n", + "- `search_knowledge_base(query)` - Finds relevant help articles and solutions\n", + "- `set_priority(ticket_id, priority)` - Assigns priority levels (low, medium, high, urgent)\n", + "- `route_to_team(ticket_id, team)` - Routes tickets to the appropriate support team\n", + "- `draft_response(ticket_id, response_text)` - Creates customer-facing responses\n", + "- `mark_complete(ticket_id)` - Finalizes processed tickets\n", + "\n", + "For a customer service agent, these tools enable processing tickets systematically. Each ticket requires classification, research, prioritization, routing, and response drafting. When processing 20-30 tickets in sequence, the conversation history fills with tool results from every classification, every knowledge base search, and every drafted response, causing linear token growth.\n", + "\n", + "The `beta_tool` decorator is used on the tools to make them accessible to the Claude agent. The decorator extracts the function arguments and docstring and provides these to Claude as tool metadata.\n", + "\n", + "```python\n", + "import anthropic\n", + "from anthropic import beta_tool\n", + "\n", + "@beta_tool\n", + "def get_next_ticket() -> dict:\n", + " \"\"\"Retrieve the next unprocessed support ticket from the queue.\"\"\"\n", + " ...\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "58d2a922", + "metadata": {}, + "outputs": [], + "source": [ + "import anthropic\n", + "from utils.customer_service_tools import (\n", + " classify_ticket,\n", + " draft_response,\n", + " get_next_ticket,\n", + " initialize_ticket_queue,\n", + " mark_complete,\n", + " route_to_team,\n", + " search_knowledge_base,\n", + " set_priority,\n", + ")\n", + "\n", + "client = anthropic.Anthropic()\n", + "\n", + "tools = [\n", + " get_next_ticket,\n", + " classify_ticket,\n", + " search_knowledge_base,\n", + " set_priority,\n", + " route_to_team,\n", + " draft_response,\n", + " mark_complete,\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "77fecfb8", + "metadata": {}, + "source": [ + "## Baseline: Running Without Compaction\n", + "\n", + "Let's start with a realistic customer service scenario: Processing a queue of support tickets. \n", + "\n", + "The workflow looks like this:\n", + "\n", + "**For Each Ticket:**\n", + "1. Fetch the ticket using `get_next_ticket()`\n", + "2. Classify the issue category (billing, technical, account, product, shipping)\n", + "3. Search the knowledge base for relevant information\n", + "4. Set appropriate priority (low, medium, high, urgent)\n", + "5. Route to the correct team\n", + "6. Draft a customer response\n", + "7. Mark the ticket complete\n", + "8. Move to the next ticket\n", + "\n", + "**The Challenge**: With 5 tickets in the queue, and each requiring 7 tool calls, Claude will make 35 or more tool calls. The results from each step including classification knowledge base search, and drafted responses accumulate in the conversation history. Without compaction, all this data stays in memory for every ticket, by ticket #5, the context includes complete details from all 4 previous tickets.\n", + "\n", + "Let's run this workflow **without compaction** first and observe what happens:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "uef86nvtl4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Turn 1: Input= 1,537 tokens | Output= 57 tokens | Messages= 1 | Cumulative In= 1,537\n", + "Turn 2: Input= 1,760 tokens | Output= 102 tokens | Messages= 3 | Cumulative In= 3,297\n", + "Turn 3: Input= 1,905 tokens | Output= 88 tokens | Messages= 5 | Cumulative In= 5,202\n", + "Turn 4: Input= 2,237 tokens | Output= 84 tokens | Messages= 7 | Cumulative In= 7,439\n", + "Turn 5: Input= 2,385 tokens | Output= 89 tokens | Messages= 9 | Cumulative In= 9,824\n", + "Turn 6: Input= 2,537 tokens | Output= 301 tokens | Messages=11 | Cumulative In= 12,361\n", + "Turn 7: Input= 2,888 tokens | Output= 67 tokens | Messages=13 | Cumulative In= 15,249\n", + "Turn 8: Input= 3,079 tokens | Output= 56 tokens | Messages=15 | Cumulative In= 18,328\n", + "Turn 9: Input= 3,316 tokens | Output= 91 tokens | Messages=17 | Cumulative In= 21,644\n", + "Turn 10: Input= 3,450 tokens | Output= 84 tokens | Messages=19 | Cumulative In= 25,094\n", + "Turn 11: Input= 3,777 tokens | Output= 84 tokens | Messages=21 | Cumulative In= 28,871\n", + "Turn 12: Input= 3,925 tokens | Output= 89 tokens | Messages=23 | Cumulative In= 32,796\n", + "Turn 13: Input= 4,077 tokens | Output= 349 tokens | Messages=25 | Cumulative In= 36,873\n", + "Turn 14: Input= 4,476 tokens | Output= 67 tokens | Messages=27 | Cumulative In= 41,349\n", + "Turn 15: Input= 4,668 tokens | Output= 56 tokens | Messages=29 | Cumulative In= 46,017\n", + "Turn 16: Input= 4,894 tokens | Output= 91 tokens | Messages=31 | Cumulative In= 50,911\n", + "Turn 17: Input= 5,028 tokens | Output= 84 tokens | Messages=33 | Cumulative In= 55,939\n", + "Turn 18: Input= 5,333 tokens | Output= 84 tokens | Messages=35 | Cumulative In= 61,272\n", + "Turn 19: Input= 5,481 tokens | Output= 89 tokens | Messages=37 | Cumulative In= 66,753\n", + "Turn 20: Input= 5,633 tokens | Output= 334 tokens | Messages=39 | Cumulative In= 72,386\n", + "Turn 21: Input= 6,017 tokens | Output= 67 tokens | Messages=41 | Cumulative In= 78,403\n", + "Turn 22: Input= 6,209 tokens | Output= 56 tokens | Messages=43 | Cumulative In= 84,612\n", + "Turn 23: Input= 6,435 tokens | Output= 91 tokens | Messages=45 | Cumulative In= 91,047\n", + "Turn 24: Input= 6,569 tokens | Output= 84 tokens | Messages=47 | Cumulative In= 97,616\n", + "Turn 25: Input= 6,896 tokens | Output= 84 tokens | Messages=49 | Cumulative In= 104,512\n", + "Turn 26: Input= 7,044 tokens | Output= 89 tokens | Messages=51 | Cumulative In= 111,556\n", + "Turn 27: Input= 7,196 tokens | Output= 372 tokens | Messages=53 | Cumulative In= 118,752\n", + "Turn 28: Input= 7,618 tokens | Output= 67 tokens | Messages=55 | Cumulative In= 126,370\n", + "Turn 29: Input= 7,808 tokens | Output= 56 tokens | Messages=57 | Cumulative In= 134,178\n", + "Turn 30: Input= 8,040 tokens | Output= 96 tokens | Messages=59 | Cumulative In= 142,218\n", + "Turn 31: Input= 8,179 tokens | Output= 85 tokens | Messages=61 | Cumulative In= 150,397\n", + "Turn 32: Input= 8,508 tokens | Output= 84 tokens | Messages=63 | Cumulative In= 158,905\n", + "Turn 33: Input= 8,656 tokens | Output= 89 tokens | Messages=65 | Cumulative In= 167,561\n", + "Turn 34: Input= 8,808 tokens | Output= 332 tokens | Messages=67 | Cumulative In= 176,369\n", + "Turn 35: Input= 9,190 tokens | Output= 67 tokens | Messages=69 | Cumulative In= 185,559\n", + "Turn 36: Input= 9,382 tokens | Output= 60 tokens | Messages=71 | Cumulative In= 194,941\n", + "Turn 37: Input= 9,475 tokens | Output= 297 tokens | Messages=73 | Cumulative In= 204,416\n", + "\n", + "============================================================\n", + "BASELINE RESULTS (NO COMPACTION)\n", + "============================================================\n", + "Total turns: 37\n", + "Input tokens: 204,416\n", + "Output tokens: 4,422\n", + "Total tokens: 208,838\n", + "============================================================\n" + ] + } + ], + "source": [ + "from anthropic.types.beta import BetaMessageParam\n", + "\n", + "num_tickets = 5\n", + "initialize_ticket_queue(num_tickets)\n", + "\n", + "messages: list[BetaMessageParam] = [\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": f\"\"\"You are an AI customer service agent. Your task is to process support tickets from a queue.\n", + "\n", + "For EACH ticket, you must complete ALL these steps:\n", + "\n", + "1. **Fetch ticket**: Call get_next_ticket() to retrieve the next unprocessed ticket\n", + "2. **Classify**: Call classify_ticket() to categorize the issue (billing/technical/account/product/shipping)\n", + "3. **Research**: Call search_knowledge_base() to find relevant information for this ticket type\n", + "4. **Prioritize**: Call set_priority() to assign priority (low/medium/high/urgent) based on severity\n", + "5. **Route**: Call route_to_team() to assign to the appropriate team\n", + "6. **Draft**: Call draft_response() to create a helpful customer response using KB information\n", + "7. **Complete**: Call mark_complete() to finalize this ticket\n", + "8. **Continue**: Immediately fetch the next ticket and repeat\n", + "\n", + "IMPORTANT RULES:\n", + "- Process tickets ONE AT A TIME in sequence\n", + "- Complete ALL 7 steps for each ticket before moving to the next\n", + "- Keep fetching and processing tickets until you get an error that the queue is empty\n", + "- There are {num_tickets} tickets total - process all of them\n", + "- Be thorough but efficient\n", + "\n", + "Begin by fetching the first ticket.\"\"\",\n", + " }\n", + "]\n", + "\n", + "total_input = 0\n", + "total_output = 0\n", + "turn_count = 0\n", + "\n", + "runner = client.beta.messages.tool_runner(\n", + " model=MODEL,\n", + " max_tokens=4096,\n", + " tools=tools,\n", + " messages=messages,\n", + ")\n", + "\n", + "for message in runner:\n", + " messages_list = list(runner._params[\"messages\"])\n", + " turn_count += 1\n", + " total_input += message.usage.input_tokens\n", + " total_output += message.usage.output_tokens\n", + " print(\n", + " f\"Turn {turn_count:2d}: Input={message.usage.input_tokens:7,} tokens | \"\n", + " f\"Output={message.usage.output_tokens:5,} tokens | \"\n", + " f\"Messages={len(messages_list):2d} | \"\n", + " f\"Cumulative In={total_input:8,}\"\n", + " )\n", + "\n", + "print(f\"\\n{'=' * 60}\")\n", + "print(\"BASELINE RESULTS (NO COMPACTION)\")\n", + "print(f\"{'=' * 60}\")\n", + "print(f\"Total turns: {turn_count}\")\n", + "print(f\"Input tokens: {total_input:,}\")\n", + "print(f\"Output tokens: {total_output:,}\")\n", + "print(f\"Total tokens: {total_input + total_output:,}\")\n", + "print(f\"{'=' * 60}\")" + ] + }, + { + "cell_type": "markdown", + "id": "3dd25c6f", + "metadata": {}, + "source": [ + "Now that we have our baseline, we have a better picture of how context grows without compaction. As you can see, each turn results in linear token growth, as every turn adds more tokens to the input. \n", + "\n", + "This leads to high token consumption and potential context limits being reached quickly. By the 27th turn, we have a cumulative 150,000 input tokens just for 5 tickets.\n", + "\n", + "Let's review Claude's final response after processing all 5 tickets without compaction:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d8b51b65", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---\n", + "\n", + "## ✅ ALL TICKETS PROCESSED SUCCESSFULLY!\n", + "\n", + "**Summary of Completed Work:**\n", + "\n", + "I have successfully processed all 5 tickets from the queue. Here's what was accomplished:\n", + "\n", + "1. **TICKET-1** - Sam Smith - Payment method update error\n", + " - Category: Billing | Priority: High | Team: billing-team\n", + " \n", + "2. **TICKET-2** - Morgan Johnson - Missing delivery\n", + " - Category: Shipping | Priority: High | Team: logistics-team\n", + " \n", + "3. **TICKET-3** - Morgan Jones - Email address change request\n", + " - Category: Account | Priority: Medium | Team: account-services\n", + " \n", + "4. **TICKET-4** - Alex Johnson - Wrong item delivered\n", + " - Category: Shipping | Priority: High | Team: logistics-team\n", + " \n", + "5. **TICKET-5** - Morgan Jones - Refund request for cancelled subscription\n", + " - Category: Billing | Priority: High | Team: billing-team\n", + "\n", + "Each ticket was:\n", + "✅ Classified correctly\n", + "✅ Researched in the knowledge base\n", + "✅ Assigned appropriate priority\n", + "✅ Routed to the correct team\n", + "✅ Given a detailed, helpful customer response\n", + "✅ Marked as complete\n", + "\n", + "The queue is now empty and all tickets have been processed!\n" + ] + } + ], + "source": [ + "print(message.content[-1].text)" + ] + }, + { + "cell_type": "markdown", + "id": "klok33ohsvn", + "metadata": {}, + "source": [ + "### Understanding the Problem\n", + "\n", + "In the baseline workflow above, Claude had to:\n", + "- Process **5 support tickets** sequentially\n", + "- Complete **7 steps per ticket** (fetch, classify, research, prioritize, route, draft, complete)\n", + "- Make **35 tool calls** with results accumulating in conversation history\n", + "- Store **every classification, every knowledge base search, every drafted response** in memory\n", + "\n", + "**Why This Happens**:\n", + "1. **Linear token growth** - With each tool use, the entire conversation history (including all previous tool results) is sent to Claude\n", + "2. **Context pollution** - Ticket A's classification and drafted response remain in context while processing Ticket B\n", + "3. **Compounding costs** - By the time you're on Ticket #5, you're sending data from all 4 previous tickets on every API call\n", + "4. **Slower responses** - Processing massive contexts takes longer\n", + "5. **Risk of hitting limits** - Eventually you hit the 200k token context window\n", + "\n", + "\n", + "**What We Actually Need**: After completing Ticket A, we only need a **brief summary** (ticket resolved, category, priority) - not the full classification result, knowledge base search, and complete drafted response. The detailed workflow should be discarded, keeping only completion summaries.\n", + "\n", + "Let's see how automatic context compaction solves this problem." + ] + }, + { + "cell_type": "markdown", + "id": "byut5h7hi3", + "metadata": {}, + "source": [ + "## Enabling Automatic Context Compaction\n", + "\n", + "Let's run the exact same customer service workflow, but with automatic context compaction enabled. We simply add the `compaction_control` parameter to our tool runner.\n", + "\n", + "The `compaction_control` parameter has one required field and several optional ones:\n", + "\n", + "- **`enabled`** (required): Boolean to turn compaction on/off\n", + "- **`context_token_threshold`** (optional): Token count that triggers compaction (default: 100,000)\n", + "- **`model`** (optional): Model to use for summarization (defaults to the main model)\n", + "- **`summary_prompt`** (optional): Custom prompt for generating summaries\n", + "\n", + "For this customer service workflow, we'll use a **5,000 token threshold**. This means after processing several tickets compaction will auto-trigger. This allows Claude to:\n", + "1. **Keep completion summaries** (tickets resolved, categories, outcomes)\n", + "2. **Discard detailed tool results** (full KB articles, complete classifications, drafted response text)\n", + "3. **Start fresh** when processing the next batch of tickets\n", + "\n", + "This mimics how a real support agent works: resolve the ticket, document it briefly, move to the next case." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "x6lnx8d20fr", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Turn 1: Input= 1,537 tokens | Output= 57 tokens | Messages= 1 | Cumulative In= 1,537\n", + "Turn 2: Input= 1,755 tokens | Output= 108 tokens | Messages= 3 | Cumulative In= 3,292\n", + "Turn 3: Input= 1,906 tokens | Output= 88 tokens | Messages= 5 | Cumulative In= 5,198\n", + "Turn 4: Input= 2,216 tokens | Output= 84 tokens | Messages= 7 | Cumulative In= 7,414\n", + "Turn 5: Input= 2,364 tokens | Output= 89 tokens | Messages= 9 | Cumulative In= 9,778\n", + "Turn 6: Input= 2,516 tokens | Output= 332 tokens | Messages=11 | Cumulative In= 12,294\n", + "Turn 7: Input= 2,898 tokens | Output= 67 tokens | Messages=13 | Cumulative In= 15,192\n", + "Turn 8: Input= 3,090 tokens | Output= 56 tokens | Messages=15 | Cumulative In= 18,282\n", + "Turn 9: Input= 3,325 tokens | Output= 97 tokens | Messages=17 | Cumulative In= 21,607\n", + "Turn 10: Input= 3,465 tokens | Output= 90 tokens | Messages=19 | Cumulative In= 25,072\n", + "Turn 11: Input= 3,801 tokens | Output= 84 tokens | Messages=21 | Cumulative In= 28,873\n", + "Turn 12: Input= 3,949 tokens | Output= 89 tokens | Messages=23 | Cumulative In= 32,822\n", + "Turn 13: Input= 4,101 tokens | Output= 368 tokens | Messages=25 | Cumulative In= 36,923\n", + "Turn 14: Input= 4,519 tokens | Output= 67 tokens | Messages=27 | Cumulative In= 41,442\n", + "Turn 15: Input= 4,711 tokens | Output= 57 tokens | Messages=29 | Cumulative In= 46,153\n", + "Turn 16: Input= 4,934 tokens | Output= 97 tokens | Messages=31 | Cumulative In= 51,087\n", + "\n", + "============================================================\n", + "🔄 Compaction occurred! Messages: 31 → 1\n", + " Summary message after compaction:\n", + "\n", + "## Support Ticket Processing Progress Summary\n", + "\n", + "### Task Overview\n", + "Processing 5 support tickets sequentially, completing all 7 steps for each ticket (fetch, classify, research, prioritize, route, draft, complete).\n", + "\n", + "### Tickets Completed (2 of 5)\n", + "\n", + "**TICKET-1 (Chris Davis) - COMPLETED**\n", + "- Issue: Account locked, unlock email link not working\n", + "- Category: account\n", + "- Priority: high\n", + "- Team: account-services\n", + "- Status: resolved\n", + "- Response: Provided guidance on checking spam folder, link expiration (1 hour), and requesting new unlock link\n", + "\n", + "**TICKET-2 (Chris Williams) - COMPLETED**\n", + "- Issue: Unrecognized $49.99 charge on 2025-10-30\n", + "- Category: billing\n", + "- Priority: high\n", + "- Team: billing-team\n", + "- Status: resolved\n", + "- Response: Explained billing cycles, subscription possibility, and refund policy (5-7 business days, pro-rated for annual plans)\n", + "\n", + "### Current Status\n", + "**TICKET-3 (John Jones) - IN PROGRESS**\n", + "- Issue: Asking about Google Sheets integration for project management\n", + "- Category: product\n", + "- Priority: NOT YET SET\n", + "- Team: NOT YET ASSIGNED\n", + "- Steps completed: 1 (fetch), 2 (classify)\n", + "- Steps remaining: 3 (research KB), 4 (set priority), 5 (route), 6 (draft), 7 (mark complete)\n", + "\n", + "### Next Steps\n", + "1. Complete TICKET-3: Search knowledge base for product integration info\n", + "2. Set priority (likely low/medium for feature inquiry)\n", + "3. Route to product-team\n", + "4. Draft response about integrations\n", + "5. Mark complete\n", + "6. Fetch and process TICKET-4\n", + "7. Fetch and process TICKET-5\n", + "\n", + "### Key Knowledge Base Info Learned\n", + "- Account: Password reset links expire in 1 hour, sent from noreply@support.example.com\n", + "- Billing: Refunds take 5-7 business days, pro-rated for annual plans, billing on same date monthly/yearly\n", + "\n", + "### Remaining Work\n", + "3 tickets left to process (TICKET-3 currently in progress, then TICKET-4 and TICKET-5)\n", + "\n", + "\n", + "============================================================\n", + "Turn 17: Input= 1,774 tokens | Output= 94 tokens | Messages= 1 | Cumulative In= 52,861\n", + "Turn 18: Input= 1,906 tokens | Output= 95 tokens | Messages= 3 | Cumulative In= 54,767\n", + "Turn 19: Input= 2,365 tokens | Output= 431 tokens | Messages= 5 | Cumulative In= 57,132\n", + "Turn 20: Input= 3,164 tokens | Output= 60 tokens | Messages= 7 | Cumulative In= 60,296\n", + "Turn 21: Input= 3,383 tokens | Output= 160 tokens | Messages= 9 | Cumulative In= 63,679\n", + "Turn 22: Input= 3,872 tokens | Output= 447 tokens | Messages=11 | Cumulative In= 67,551\n", + "Turn 23: Input= 4,687 tokens | Output= 64 tokens | Messages=13 | Cumulative In= 72,238\n", + "Turn 24: Input= 4,914 tokens | Output= 160 tokens | Messages=15 | Cumulative In= 77,152\n", + "\n", + "============================================================\n", + "🔄 Compaction occurred! Messages: 15 → 1\n", + " Summary message after compaction:\n", + "\n", + "## Support Ticket Processing Progress Summary\n", + "\n", + "### Task Overview\n", + "Processing 5 support tickets sequentially, completing all 7 steps for each ticket (fetch, classify, research, prioritize, route, draft, complete).\n", + "\n", + "### Tickets Completed (4 of 5)\n", + "\n", + "**TICKET-1 (Chris Davis) - COMPLETED**\n", + "- Issue: Account locked, unlock email link not working\n", + "- Category: account\n", + "- Priority: high\n", + "- Team: account-services\n", + "- Status: resolved\n", + "- Response: Provided guidance on checking spam folder, link expiration (1 hour), and requesting new unlock link\n", + "\n", + "**TICKET-2 (Chris Williams) - COMPLETED**\n", + "- Issue: Unrecognized $49.99 charge on 2025-10-30\n", + "- Category: billing\n", + "- Priority: high\n", + "- Team: billing-team\n", + "- Status: resolved\n", + "- Response: Explained billing cycles, subscription possibility, and refund policy (5-7 business days, pro-rated for annual plans)\n", + "\n", + "**TICKET-3 (John Jones) - COMPLETED**\n", + "- Issue: Asking about Google Sheets integration for project management\n", + "- Category: product\n", + "- Priority: medium\n", + "- Team: product-success\n", + "- Status: resolved\n", + "- Response: Explained that Product Success team will provide details on integration options, API access, and current/planned features\n", + "\n", + "**TICKET-4 (Sam Johnson) - COMPLETED**\n", + "- Issue: Wants to know differences between Standard and Premium plans, specifically \"advanced analytics\"\n", + "- Category: product\n", + "- Priority: low\n", + "- Team: product-success\n", + "- Status: resolved\n", + "- Response: Explained that Product Success team will provide detailed plan comparison and feature breakdown\n", + "\n", + "### Current Status\n", + "**TICKET-5 (Morgan Brown) - IN PROGRESS**\n", + "- Issue: Damaged package (Order #ORD-43312), broken product inside, needs replacement\n", + "- Category: shipping (classified)\n", + "- Priority: NOT YET SET\n", + "- Team: NOT YET ASSIGNED\n", + "- Steps completed: 1 (fetch), 2 (classify), 3 (research KB - no shipping info found)\n", + "- Steps remaining: 4 (set priority), 5 (route), 6 (draft), 7 (mark complete)\n", + "\n", + "### Next Steps for TICKET-5\n", + "1. Set priority (likely HIGH - damaged/broken product requiring replacement)\n", + "2. Route to appropriate team (likely fulfillment, operations, or customer-service team)\n", + "3. Draft response addressing damaged shipment, replacement process, and next steps\n", + "4. Mark complete\n", + "5. **ALL TICKETS WILL BE COMPLETE**\n", + "\n", + "### Key Knowledge Base Info Learned\n", + "- **Account**: Password reset links expire in 1 hour, sent from noreply@support.example.com\n", + "- **Billing**: Refunds take 5-7 business days, pro-rated for annual plans, billing on same date monthly/yearly; accepts Visa, Mastercard, Amex, PayPal\n", + "- **Technical**: Max upload 100MB, supported formats: PDF, DOCX, PNG, JPG, CSV; system requirements: 4GB RAM, modern browsers\n", + "- **Product category**: Does not exist in KB (only billing, technical, account available)\n", + "- **Shipping info**: Not found in knowledge base\n", + "\n", + "### Team Routing Patterns Observed\n", + "- account-services: Account access issues\n", + "- billing-team: Billing/payment inquiries\n", + "- product-success: Product features, integrations, plan comparisons\n", + "\n", + "### Remaining Work\n", + "1 ticket left to complete (TICKET-5 - final ticket, currently in progress at step 3 of 7)\n", + "\n", + "\n", + "============================================================\n", + "Turn 25: Input= 2,077 tokens | Output= 496 tokens | Messages= 1 | Cumulative In= 79,229\n", + "Turn 26: Input= 2,942 tokens | Output= 438 tokens | Messages= 3 | Cumulative In= 82,171\n", + "\n", + "============================================================\n", + "OPTIMIZED RESULTS (WITH COMPACTION)\n", + "============================================================\n", + "Total turns: 26\n", + "Compactions: 2\n", + "Input tokens: 82,171\n", + "Output tokens: 4,275\n", + "Total tokens: 86,446\n", + "============================================================\n" + ] + } + ], + "source": [ + "# Re-initialize queue and run with compaction\n", + "initialize_ticket_queue(num_tickets)\n", + "\n", + "total_input_compact = 0\n", + "total_output_compact = 0\n", + "turn_count_compact = 0\n", + "compaction_count = 0\n", + "prev_msg_count = 0\n", + "\n", + "runner = client.beta.messages.tool_runner(\n", + " model=MODEL,\n", + " max_tokens=4096,\n", + " tools=tools,\n", + " messages=messages,\n", + " compaction_control={\n", + " \"enabled\": True,\n", + " \"context_token_threshold\": 5000,\n", + " },\n", + ")\n", + "\n", + "for message in runner:\n", + " turn_count_compact += 1\n", + " total_input_compact += message.usage.input_tokens\n", + " total_output_compact += message.usage.output_tokens\n", + " messages_list = list(runner._params[\"messages\"])\n", + " curr_msg_count = len(messages_list)\n", + "\n", + " if curr_msg_count < prev_msg_count:\n", + " # We can identify compaction when the message count decreases\n", + " compaction_count += 1\n", + "\n", + " print(f\"\\n{'=' * 60}\")\n", + " print(f\"🔄 Compaction occurred! Messages: {prev_msg_count} → {curr_msg_count}\")\n", + " print(\" Summary message after compaction:\")\n", + " print(messages_list[-1][\"content\"][-1].text) # type: ignore\n", + " print(f\"\\n{'=' * 60}\")\n", + "\n", + " prev_msg_count = curr_msg_count\n", + " print(\n", + " f\"Turn {turn_count_compact:2d}: Input={message.usage.input_tokens:7,} tokens | \"\n", + " f\"Output={message.usage.output_tokens:5,} tokens | \"\n", + " f\"Messages={len(messages_list):2d} | \"\n", + " f\"Cumulative In={total_input_compact:8,}\"\n", + " )\n", + "\n", + "print(f\"\\n{'=' * 60}\")\n", + "print(\"OPTIMIZED RESULTS (WITH COMPACTION)\")\n", + "print(f\"{'=' * 60}\")\n", + "print(f\"Total turns: {turn_count_compact}\")\n", + "print(f\"Compactions: {compaction_count}\")\n", + "print(f\"Input tokens: {total_input_compact:,}\")\n", + "print(f\"Output tokens: {total_output_compact:,}\")\n", + "print(f\"Total tokens: {total_input_compact + total_output_compact:,}\")\n", + "print(f\"{'=' * 60}\")" + ] + }, + { + "cell_type": "markdown", + "id": "5cbbfe32", + "metadata": {}, + "source": [ + "With automatic context compaction enabled, we can see that our token usage per turn does not grow linearly, but is reduced after each compaction event. There were two compaction events during the processing of tickets, and the follow turn shows a reduction in total token usage.\n", + "\n", + "Compared to the baseline version, we only used 79,000 tokens. We've also printed out the summary messages generated after each compaction event, showing how Claude effectively condensed prior ticket details into summaries.\n", + "\n", + "Let's look at the final response after processing all 5 tickets with compaction enabled." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "24dd5c7c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Perfect! **ALL 5 TICKETS HAVE BEEN SUCCESSFULLY COMPLETED!** 🎉\n", + "\n", + "## Final Summary - All Tickets Processed\n", + "\n", + "### TICKET-5 (Morgan Brown) - **COMPLETED** ✓\n", + "- **Issue**: Damaged package (Order #ORD-43312), broken product inside, needs replacement\n", + "- **Category**: shipping\n", + "- **Priority**: high\n", + "- **Team**: logistics-team\n", + "- **Status**: resolved\n", + "- **Response**: Apologized for damaged shipment, escalated to Logistics Team with HIGH priority, explained they'll process immediate replacement, provide return instructions, and contact customer with tracking and timeline\n", + "\n", + "---\n", + "\n", + "## 🎯 ALL 5 TICKETS COMPLETED\n", + "\n", + "1. ✅ **TICKET-1** (Chris Davis) - Account locked → account-services\n", + "2. ✅ **TICKET-2** (Chris Williams) - Billing charge → billing-team \n", + "3. ✅ **TICKET-3** (John Jones) - Google Sheets integration → product-success\n", + "4. ✅ **TICKET-4** (Sam Johnson) - Plan comparison → product-success\n", + "5. ✅ **TICKET-5** (Morgan Brown) - Damaged shipment → logistics-team\n", + "\n", + "### Processing Statistics\n", + "- **Total tickets processed**: 5 of 5 (100%)\n", + "- **Steps per ticket**: 7 (fetch, classify, research, prioritize, route, draft, complete)\n", + "- **Total operations**: 35 successful operations\n", + "- **Categories used**: account, billing, product (2x), shipping\n", + "- **Teams utilized**: account-services, billing-team, product-success (2x), logistics-team\n", + "- **Priority distribution**: 2 high, 2 medium, 1 low\n", + "\n", + "All tickets have been properly classified, prioritized, routed to the appropriate teams, and have draft responses ready for team review! 🎊\n" + ] + } + ], + "source": [ + "print(message.content[-1].text)" + ] + }, + { + "cell_type": "markdown", + "id": "vb4cesnmb8", + "metadata": {}, + "source": [ + "### Comparing Results\n", + "\n", + "With compaction enabled, we can see a clear differece between the two runs in token savings, while preserving the quality of the workflow and final summary.\n", + "\n", + "Here's what changed with automatic context compaction:\n", + "\n", + "1. **Context resets after several tickets** - When processing 5-7 tickets generates 5k+ tokens of tool results, the SDK automatically:\n", + " - Injects a summary prompt\n", + " - Has Claude generate a completion summary wrapped in `` tags\n", + " - Clears the conversation history and discards detailed classifications, KB searches, and responses\n", + " - Continues with only the completion summary\n", + "\n", + "2. **Input tokens stay bounded** - Instead of accumulating to 100k+ as we process more tickets, input tokens reset after each compaction. When processing Ticket #5, we're NOT carrying the full tool results from Tickets #1-4.\n", + "\n", + "3. **Task completes successfully** - The workflow continues smoothly through all tickets without hitting context limits\n", + "\n", + "4. **Quality is preserved** - The summaries retain critical information:\n", + " - Tickets processed with their IDs\n", + " - Categories and priorities assigned\n", + " - Teams routed to\n", + " - Overall progress status\n", + " \n", + " All tickets are still properly classified, prioritized, routed, and responded to.\n", + "\n", + "5. **Natural workflow** - This mirrors how real support agents work: resolve a ticket, document it briefly in the system, close it, move to the next one. You don't keep every knowledge base article and full response draft open while working on new tickets.\n", + "\n", + "Let's visualize the token savings:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "z9lvigc94p", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "======================================================================\n", + "TOKEN USAGE COMPARISON\n", + "======================================================================\n", + "Metric Baseline With Compaction \n", + "----------------------------------------------------------------------\n", + "Input tokens: 204,416 82,171\n", + "Output tokens: 4,422 4,275\n", + "Total tokens: 208,838 86,446\n", + "Compactions: N/A 2\n", + "======================================================================\n", + "\n", + "💰 Token Savings: 122,392 tokens (58.6% reduction)\n" + ] + } + ], + "source": [ + "# Compare baseline vs compaction\n", + "print(\"=\" * 70)\n", + "print(\"TOKEN USAGE COMPARISON\")\n", + "print(\"=\" * 70)\n", + "print(f\"{'Metric':<30} {'Baseline':<20} {'With Compaction':<20}\")\n", + "print(\"-\" * 70)\n", + "print(f\"{'Input tokens:':<30} {total_input:>19,} {total_input_compact:>19,}\")\n", + "print(f\"{'Output tokens:':<30} {total_output:>19,} {total_output_compact:>19,}\")\n", + "print(\n", + " f\"{'Total tokens:':<30} {total_input + total_output:>19,} {total_input_compact + total_output_compact:>19,}\"\n", + ")\n", + "print(f\"{'Compactions:':<30} {'N/A':>19} {compaction_count:>19}\")\n", + "print(\"=\" * 70)\n", + "\n", + "# Calculate savings\n", + "token_savings = (total_input + total_output) - (total_input_compact + total_output_compact)\n", + "savings_percent = (\n", + " (token_savings / (total_input + total_output)) * 100 if (total_input + total_output) > 0 else 0\n", + ")\n", + "\n", + "print(f\"\\n💰 Token Savings: {token_savings:,} tokens ({savings_percent:.1f}% reduction)\")" + ] + }, + { + "cell_type": "markdown", + "id": "lzvf1mw7o6", + "metadata": {}, + "source": [ + "## How Compaction Works Under the Hood\n", + "\n", + "When the `tool_runner` detects that token usage has exceeded the threshold, it automatically:\n", + "\n", + "1. **Pauses the workflow** before making the next API call\n", + "2. **Injects a summary request** as a user message asking Claude to summarize progress\n", + "3. **Generates a summary** - Claude produces a summary wrapped in `` tags containing:\n", + " - **Completed tickets**: Brief records of tickets resolved (IDs, categories, priorities, outcomes)\n", + " - **Progress status**: How many tickets processed, how many remain\n", + " - **Key patterns**: Any notable trends across tickets\n", + " - **Next steps**: What to do next (continue processing remaining tickets)\n", + "4. **Clears history** - The entire conversation history (including all tool results) is replaced with just the summary\n", + "5. **Resumes processing** - Claude continues working with the compressed context, processing the next batch of tickets" + ] + }, + { + "cell_type": "markdown", + "id": "v64ljd0a79", + "metadata": {}, + "source": [ + "## Customizing Compaction Configuration\n", + "\n", + "You can customize how compaction works to fit your specific use case. Here are the key configuration options:\n", + "\n", + "### Adjusting the Threshold\n", + "\n", + "The `context_token_threshold` determines when compaction triggers:\n", + "\n", + "```python\n", + "compaction_control={\n", + " \"enabled\": True,\n", + " \"context_token_threshold\": 5000, # Compact after processing 5-7 tickets\n", + "}\n", + "```\n", + "\n", + "The threshold should not be set too low, otherwise the summary itself could trigger a compaction. We set a threshold of 5,000 tokens for demonstration purposes, but in practice, experiment with different settings to find what works best for your workflow.\n", + "\n", + "Here some general guidelines:\n", + "\n", + "- **Low thresholds (5k-20k)**: \n", + " - Use for iterative task processing with clear boundaries\n", + " - More frequent compaction, minimal context accumulation\n", + " - Best for sequential entity processing\n", + " \n", + "- **Medium thresholds (50k-100k)**: \n", + " - Multi-phase workflows with fewer, larger natural checkpoints\n", + " - Balance between context retention and management\n", + " - Suitable for workflows with expensive tool calls\n", + " \n", + "- **High thresholds (100k-150k)**: \n", + " - Tasks requiring substantial historical context\n", + " - Less frequent compaction preserves more raw details\n", + " - Higher per-call costs but fewer compactions\n", + " \n", + "- **Default (100k)**: Good balance for general long-running tasks\n", + "\n", + "**For ticket processing**: The 5k threshold works well because each ticket's workflow generates substantial tool results, but tickets are independent. After resolving Ticket A, you don't need its detailed KB searches when processing Ticket B.\n", + "\n", + "### Using a Different Model for Summarization\n", + "\n", + "You can also use a faster/cheaper model for generating summaries:\n", + "\n", + "```python\n", + "compaction_control={\n", + " \"enabled\": True,\n", + " \"model\": \"claude-haiku-4-5\", # Use Haiku for cost-effective summaries\n", + "}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "w4oyvorzkn", + "metadata": {}, + "source": [ + "### Custom Summary Prompts\n", + "\n", + "You can provide a custom prompt to guide how summaries are generated. This is especially useful for customer service workflows where you need to preserve specific types of information.\n", + "\n", + "For example, we could define a custom prompt based on our requirements:\n", + "- **Ticket summaries** for all completed tickets\n", + "- **Categories and priorities** assigned\n", + "- **Teams routed to**\n", + "- **Progress status** (tickets completed, tickets remaining)\n", + "- **Next steps** in the workflow\n", + "\n", + "```python\n", + "compaction_control={\n", + " \"enabled\": True,\n", + " \"summary_prompt\": \"\"\"You are processing customer support tickets from a queue.\n", + "\n", + "Create a focused summary that preserves:\n", + "\n", + "1. **COMPLETED TICKETS**: For each ticket you've fully processed:\n", + " - Ticket ID and customer name\n", + " - Issue category and priority assigned\n", + " - Team routed to\n", + " - Brief outcome\n", + "\n", + "2. **PROGRESS STATUS**: \n", + " - How many tickets you've completed\n", + " - Approximately how many remain in the queue\n", + "\n", + "3. **NEXT STEPS**: Continue processing the next ticket\n", + "\n", + "Format with clear sections and wrap in tags.\"\"\"\n", + "}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "7cwipn73ib9", + "metadata": {}, + "source": [ + "## Compaction Without Tools: Simple Chat Loop\n", + "\n", + "While the examples above focus on tool-heavy agentic workflows, context compaction is also valuable for **simple conversational applications** where users drive the conversation.\n", + "\n", + " **Note:** The `compaction_control` parameter demonstrated above works with `tool_runner` for agentic workflows with tools. For simple chat applications without tools, you'll implement compaction manually using the same principles.\n", + "\n", + "Consider a chat application where users are having extended conversations with Claude—discussing complex topics, iterating on ideas, or working through problems. As the conversation grows, you face the same context accumulation challenges.\n", + "\n", + "**The Difference**: Instead of tool use triggering token growth, it's the back-and-forth conversation itself. Each exchange adds messages to the history:\n", + "- User asks a question\n", + "- Claude provides a detailed response\n", + "- User asks for clarification or elaboration\n", + "- Claude responds with more context\n", + "- This repeats dozens or hundreds of times\n", + "\n", + "Without compaction, by turn 50 you're sending the entire conversation history (all 50 exchanges) on every API call.\n", + "\n", + "**The Solution**: Implement compaction manually in your chat loop using the same pattern:\n", + "1. Track token usage after each turn\n", + "2. When threshold is exceeded, request a summary\n", + "3. Replace conversation history with the summary\n", + "4. Continue the conversation with compressed context\n", + "\n", + "Let's see how to implement this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "m6akcmnsz09", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Chat with Claude (type 'quit' to exit, or just hit Enter to continue)\n", + "This is a demonstration - try having a conversation and watch compaction trigger\n", + "============================================================\n", + "\n", + "You: Help me understand how Python decorators work\n" + ] + } + ], + "source": [ + "#!/usr/bin/env python3\n", + "\"\"\"\n", + "Simple Compaction Example - User-Driven Chat Loop\n", + "\n", + "This shows the basic pattern for a chat application with compaction.\n", + "No tools required - just a simple loop where the user drives continuation.\n", + "\"\"\"\n", + "\n", + "# Configuration\n", + "COMPACTION_THRESHOLD = 3000 # Compact when tokens exceed this (low for demo purposes)\n", + "\n", + "# Structured summarization prompt for compaction\n", + "SUMMARY_PROMPT = \"\"\"You have been working on the task described above but have not yet completed it. Write a continuation summary that will allow you (or another instance of yourself) to resume work efficiently in a future context window where the conversation history will be replaced with this summary. Your summary should be structured, concise, and actionable. Include:\n", + "\n", + "1. **Task Overview**\n", + " - The user's core request and success criteria\n", + " - Any clarifications or constraints they specified\n", + "\n", + "2. **Current State**\n", + " - What has been completed so far\n", + " - Files created, modified, or analyzed (with paths if relevant)\n", + " - Key outputs or artifacts produced\n", + "\n", + "3. **Important Discoveries**\n", + " - Technical constraints or requirements uncovered\n", + " - Decisions made and their rationale\n", + " - Errors encountered and how they were resolved\n", + " - What approaches were tried that didn't work (and why)\n", + "\n", + "4. **Next Steps**\n", + " - Specific actions needed to complete the task\n", + " - Any blockers or open questions to resolve\n", + " - Priority order if multiple steps remain\n", + "\n", + "5. **Context to Preserve**\n", + " - User preferences or style requirements\n", + " - Domain-specific details that aren't obvious\n", + " - Any promises made to the user\n", + "\n", + "Be concise but complete—err on the side of including information that would prevent duplicate work or repeated mistakes.\n", + " Write in a way that enables immediate resumption of the task.\n", + "\n", + "Wrap your summary in tags.\"\"\"\n", + "\n", + "# Message history\n", + "messages = []\n", + "\n", + "print(\"Chat with Claude (type 'quit' to exit, or just hit Enter to continue)\")\n", + "print(\"This is a demonstration - try having a conversation and watch compaction trigger\")\n", + "print(\"=\" * 60)\n", + "\n", + "# Simulate a conversation for demo purposes\n", + "demo_messages = [\n", + " \"Help me understand how Python decorators work\",\n", + " \"Can you show me an example with a timing decorator?\",\n", + " \"How would I make a decorator that takes arguments?\",\n", + "]\n", + "\n", + "for user_input in demo_messages:\n", + " print(f\"\\nYou: {user_input}\")\n", + "\n", + " # Add user message\n", + " messages.append({\"role\": \"user\", \"content\": user_input})\n", + "\n", + " # Get Claude's response\n", + " response = client.messages.create(\n", + " model=MODEL,\n", + " max_tokens=2048,\n", + " messages=messages,\n", + " )\n", + "\n", + " messages.append(\n", + " {\n", + " \"role\": \"assistant\",\n", + " \"content\": response.content,\n", + " }\n", + " )\n", + "\n", + " print(\"\\nClaude: \", end=\"\")\n", + " for block in response.content:\n", + " if block.type == \"text\":\n", + " print(f\"{block.text[:300]} ...\")\n", + "\n", + " # Check if we should compact\n", + " usage = response.usage\n", + "\n", + " # Calculate total tokens (includes cache tokens)\n", + " total_input_tokens = (\n", + " usage.input_tokens\n", + " + (usage.cache_creation_input_tokens or 0)\n", + " + (usage.cache_read_input_tokens or 0)\n", + " )\n", + " total_tokens = total_input_tokens + usage.output_tokens\n", + "\n", + " cache_info = \"\"\n", + " if usage.cache_creation_input_tokens or usage.cache_read_input_tokens:\n", + " cache_info = f\" (cache: {usage.cache_creation_input_tokens or 0} write + {usage.cache_read_input_tokens or 0} read)\"\n", + "\n", + " print(\n", + " f\"\\n[Tokens: {total_input_tokens} in{cache_info} + {usage.output_tokens} out = {total_tokens} total]\"\n", + " )\n", + "\n", + " if total_tokens > COMPACTION_THRESHOLD:\n", + " print(f\"\\n{'=' * 60}\")\n", + " print(f\"🔄 Compacting conversation... {len(messages)} messages → \", end=\"\", flush=True)\n", + "\n", + " # Get summary using structured prompt\n", + " summary_response = client.messages.create(\n", + " model=MODEL,\n", + " max_tokens=4096,\n", + " messages=messages + [{\"role\": \"user\", \"content\": SUMMARY_PROMPT}],\n", + " )\n", + "\n", + " summary_text = \"\".join(\n", + " block.text for block in summary_response.content if block.type == \"text\"\n", + " )\n", + "\n", + " # Replace history with summary\n", + " messages = [{\"role\": \"user\", \"content\": summary_text}]\n", + "\n", + " print(\"1 message\")\n", + " print(f\"{'=' * 60}\\n\")\n", + "\n", + "print(f\"Final conversation messages: {messages[-1].get('content')}\")\n", + "\n", + "print(\"\\nDemo complete! In a real application, this loop would continue with user input.\")" + ] + }, + { + "cell_type": "markdown", + "id": "b229j75wdjm", + "metadata": {}, + "source": [ + "### Understanding the Chat Loop Pattern\n", + "\n", + "The example above demonstrates manual compaction in a conversational context. Here's how it works:\n", + "\n", + "**Key Components**:\n", + "\n", + "1. **Token Tracking**: After each response, calculate total tokens (input + output + cache tokens)\n", + "2. **Threshold Check**: When total exceeds threshold, trigger compaction\n", + "3. **Summary Request**: Send the same structured SUMMARY_PROMPT to Claude\n", + "4. **History Replacement**: Replace entire message history with just the summary\n", + "5. **Continue**: Next user message builds on the summary, not full history\n", + "\n", + "**When to Use This Pattern**:\n", + "\n", + "- **Extended brainstorming sessions**: Users exploring ideas with Claude over many turns\n", + "- **Learning conversations**: Tutorials or explanations that span dozens of exchanges\n", + "- **Iterative refinement**: Users providing feedback on drafts, designs, or solutions\n", + "- **Chat applications**: Any multi-turn conversation interface\n", + "\n", + "**Key Differences from Tool Runner**:\n", + "\n", + "| Aspect | Tool Runner (Automatic) | Chat Loop (Manual) |\n", + "|--------|------------------------|-------------------|\n", + "| **Trigger** | Automatic when threshold reached | You implement threshold check |\n", + "| **Summary** | SDK handles summary request | You make explicit API call |\n", + "| **History Management** | SDK replaces messages | You manually replace list |\n", + "| **Use Case** | Agentic workflows with tools | User-driven conversations |\n", + "\n", + "**Production Considerations**:\n", + "\n", + "1. **Adjust threshold**: Use larger thresholds for real applications\n", + "2. **Customize summary prompt**: Tailor to your conversation type (brainstorming vs. technical support vs. tutoring)\n", + "3. **Show user indicators**: Display a message like \"Summarizing conversation...\" so users understand the pause\n", + "4. **Preserve key context**: Ensure the summary prompt captures domain-specific information your users care about\n", + "\n", + "This pattern gives you full control over when and how compaction happens, making it ideal for conversational applications where the SDK's automatic tool-runner compaction isn't available." + ] + }, + { + "cell_type": "markdown", + "id": "d71dwo1dayp", + "metadata": {}, + "source": [ + "## Limitations and Considerations\n", + "\n", + "While automatic context compaction is powerful, there are important limitations to understand:\n", + "\n", + "### Server-Side Sampling Loops\n", + "\n", + "**Current Limitation**: Compaction does not work optimally with server-side sampling loops, such as server-side web search tools.\n", + "\n", + "**Why**: Cache tokens accumulate across sampling loops, which can trigger compaction prematurely based on cached content rather than actual conversation history.\n", + "\n", + "This feature works best with:\n", + "- ✅ Client-side tools (like the customer service API in this cookbook)\n", + "- ✅ Standard agentic workflows with regular tool use\n", + "- ✅ File operations, database queries, API calls\n", + "- ❌ Server-side Extended Thinking\n", + "- ❌ Server-side web search tools\n", + "\n", + "### Information Loss\n", + "\n", + "**Trade-off**: Summaries inherently lose some information. While Claude is good at identifying key points, some details will be compressed or omitted.\n", + "\n", + "**In ticket processing**: \n", + "- ✅ **Retained**: Ticket IDs, categories, priorities, teams, outcomes, progress status\n", + "- ❌ **Lost**: Full knowledge base article text, complete drafted response text, detailed classification reasoning\n", + "\n", + "This is usually acceptable, you don't need every KB article and full response text in perpetuity, just the completion records.\n", + "\n", + "**Mitigation**:\n", + "- Use custom summary prompts to preserve critical information\n", + "- Set higher thresholds for tasks requiring extensive historical context\n", + "- Structure your tasks to be modular (each phase builds on summaries, not raw details)\n", + "\n", + "### When NOT to Use Compaction\n", + "\n", + "Avoid compaction for:\n", + "\n", + "1. **Short tasks**: If your task completes within 50k-100k tokens, compaction adds unnecessary overhead\n", + "2. **Tasks requiring full audit trails**: Some tasks need access to ALL previous details\n", + "3. **Server-side sampling workflows**: As mentioned above, wait for this limitation to be addressed\n", + "4. **Highly iterative refinement**: Tasks where each step critically depends on exact details from all previous steps\n", + "\n", + "### When TO Use Compaction\n", + "\n", + "Compaction is ideal for:\n", + "\n", + "1. **Sequential processing**: Like our ticket workflow—process multiple items one after another\n", + "2. **Multi-phase workflows**: Where each phase can summarize progress before moving on\n", + "3. **Iterative data processing**: Processing large datasets in chunks or entities one at a time\n", + "4. **Extended analysis sessions**: Analyzing data across many entities\n", + "5. **Batch operations**: Processing hundreds of items where each is independent\n", + "\n", + "**Ticket processing is a perfect use case** because:\n", + "- Each ticket workflow is largely independent\n", + "- You need completion summaries, not full tool results\n", + "- Natural compaction points exist (after completing several tickets)\n", + "- The workflow is iterative and sequential" + ] + }, + { + "cell_type": "markdown", + "id": "b4pz1jmdidi", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "Automatic context compaction is a powerful feature that enables long-running agentic workflows to exceed typical context limits. In this cookbook, we've explored compaction through a customer service ticket processing workflow.\n", + "\n", + "### Next Steps\n", + "\n", + "Try implementing compaction in your own workflows:\n", + "1. Identify natural compaction points (after processing each item, completing each phase, etc.)\n", + "2. Start with an aggressive threshold (5k-10k) if you have clear per-item boundaries\n", + "3. Use custom summary prompts to preserve critical information\n", + "4. Monitor when compaction triggers and verify quality is maintained\n", + "5. Adjust threshold based on your specific needs\n", + "\n", + "For more on effective context management, see [Effective Context Engineering for AI Agents](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "anthropic-cookbook (3.12.12)", + "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.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tool_use/ptc.ipynb b/tool_use/ptc.ipynb new file mode 100644 index 00000000..686a2235 --- /dev/null +++ b/tool_use/ptc.ipynb @@ -0,0 +1,1831 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4126045b", + "metadata": {}, + "source": [ + "# Programatic Tool Calling (PTC) with the Claude API\n", + "\n", + "Programmatic Tool Calling (PTC) allows Claude to write code that calls tools programmatically within the Code Execution environment, rather than requiring round-trips through the model for each tool invocation. This substantially reduces end-to-end latency for multiple tool calls, and can dramatically reduce token consumption by allowing the model to write code that removes irrelevant context before it hits the model’s context window (for example, by grepping for key information within large and noisy files).\n", + "\n", + "When faced with third-party APIs and tools that you may not be able to modify directly, PTC can help reduce usage of context by allowing Claude to write code that can be invoked in the Code Execution environment. \n", + "\n", + "In this cookbook, we will work with a mock API for team expense management. The API is designed to require multiple invocations and will return large results which help illustrate the benefits of Programmatic Tool Calling." + ] + }, + { + "cell_type": "markdown", + "id": "4d7e647f", + "metadata": {}, + "source": [ + "## By the end of this cookbook, you'll be able to:\n", + "\n", + "- Understand the difference between regular tool calling and programatic tool calling (PTC)\n", + "- Write agents that leverage PTC \n" + ] + }, + { + "cell_type": "markdown", + "id": "e0e31236", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "Before following this guide, ensure you have:\n", + "\n", + "**Required Knowledge**\n", + "\n", + "- Python fundamentals - comfortable with async/await, functions, and basic data structures\n", + "- Basic understanding of agentic patterns and tool calling\n", + "\n", + "**Required Tools**\n", + "\n", + "- Python 3.11 or higher\n", + "- Anthropic API key\n", + "- Anthropic Python SDK >= 0.72\n" + ] + }, + { + "cell_type": "markdown", + "id": "43e53178", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First, install the required dependencies:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa190a78", + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install -r requirements.txt" + ] + }, + { + "cell_type": "markdown", + "id": "5d24e94e", + "metadata": {}, + "source": [ + "Note: Ensure your .env file contains:\n", + "\n", + "`ANTHROPIC_API_KEY=your_key_here`\n", + "\n", + "Load your environment variables and configure the client. We also load a helper utility to visualize Claude message responses.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "3a1f5033", + "metadata": {}, + "outputs": [], + "source": [ + "from dotenv import load_dotenv\n", + "from utils.visualize import visualize\n", + "\n", + "load_dotenv()\n", + "\n", + "MODEL = \"claude-sonnet-4-5\"\n", + "\n", + "viz = visualize(auto_show=True)" + ] + }, + { + "cell_type": "markdown", + "id": "8d0fc3df", + "metadata": {}, + "source": [ + "## Understanding the Third-Party API\n", + "\n", + "In [utils/team_expense_api.py](utils/team_expense_api.py), there are three functions defined: `get_team_members`, `get_expenses`, and `get_custom_budget`. The `get_team_members` function allows us to retrieve all employees in a given department with their role, level, and contact information. The `get_expenses` function returns all expense line items for an employee in a specific quarter—this can be several hundred records per employee, with each record containing extensive metadata including receipt URLs, approval chains, merchant details, and more. The `get_custom_budget` function checks if a specific employee has a custom travel budget exception (otherwise they use the standard $5,000 quarterly limit).\n", + "\n", + "In this scenario, we need to analyze team expenses and identify which employees have exceeded their budgets. Traditionally, we might manually pull expense reports for each person, sum up their expenses by category, compare against budget limits (checking for custom budget exceptions), and compile a report. Instead, we will ask Claude to perform this analysis for us, using the available tools to retrieve team data, fetch potentially hundreds of expense line items with rich metadata, and determine who has gone over budget.\n", + "\n", + "The key challenge here is that each employee may have 100+ expense line items that need to be fetched, parsed, and aggregated—and the `get_custom_budget` tool can only be called after analyzing expenses to see if someone exceeded the standard budget. This creates a sequential dependency chain that makes this an ideal use case for demonstrating the benefits of Programmatic Tool Calling.\n", + "\n", + "We'll pass our tool definitions to the messages API and ask Claude to perform the analysis. Read the docs on [implementing tool use](https://docs.claude.com/en/docs/agents-and-tools/tool-use/implement-tool-use) if you are not familiar with how tool use works with Claude's API." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "af013fcc", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "\n", + "import anthropic\n", + "from utils.team_expense_api import get_custom_budget, get_expenses, get_team_members\n", + "\n", + "client = anthropic.Anthropic()\n", + "\n", + "# Tool definitions for the team expense API\n", + "tools = [\n", + " {\n", + " \"name\": \"get_team_members\",\n", + " \"description\": 'Returns a list of team members for a given department. Each team member includes their ID, name, role, level (junior, mid, senior, staff, principal), and contact information. Use this to get a list of people whose expenses you want to analyze. Available departments are: engineering, sales, and marketing.\\n\\nRETURN FORMAT: Returns a JSON string containing an ARRAY of team member objects (not wrapped in an outer object). Parse with json.loads() to get a list. Example: [{\"id\": \"ENG001\", \"name\": \"Alice\", ...}, {\"id\": \"ENG002\", ...}]',\n", + " \"input_schema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"department\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The department name. Case-insensitive.\",\n", + " }\n", + " },\n", + " \"required\": [\"department\"],\n", + " },\n", + " \"input_examples\": [\n", + " {\"department\": \"engineering\"},\n", + " {\"department\": \"sales\"},\n", + " {\"department\": \"marketing\"},\n", + " ],\n", + " },\n", + " {\n", + " \"name\": \"get_expenses\",\n", + " \"description\": \"Returns all expense line items for a given employee in a specific quarter. Each expense includes extensive metadata: date, category, description, amount (in USD), currency, status (approved, pending, rejected), receipt URL, approval chain, merchant name and location, payment method, and project codes. An employee may have 20-50+ expense line items per quarter, and each line item contains substantial metadata for audit and compliance purposes. Categories include: 'travel' (flights, trains, rental cars, taxis, parking), 'lodging' (hotels, airbnb), 'meals', 'software', 'equipment', 'conference', 'office', and 'internet'. IMPORTANT: Only expenses with status='approved' should be counted toward budget limits.\\n\\nRETURN FORMAT: Returns a JSON string containing an ARRAY of expense objects (not wrapped in an outer object with an 'expenses' key). Parse with json.loads() to get a list directly. Example: [{\\\"expense_id\\\": \\\"ENG001_Q3_001\\\", \\\"amount\\\": 1250.50, \\\"category\\\": \\\"travel\\\", ...}, {...}]\",\n", + " \"input_schema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"employee_id\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The unique employee identifier\",\n", + " },\n", + " \"quarter\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Quarter identifier: 'Q1', 'Q2', 'Q3', or 'Q4'\",\n", + " },\n", + " },\n", + " \"required\": [\"employee_id\", \"quarter\"],\n", + " },\n", + " \"input_examples\": [\n", + " {\"employee_id\": \"ENG001\", \"quarter\": \"Q3\"},\n", + " {\"employee_id\": \"SAL002\", \"quarter\": \"Q1\"},\n", + " {\"employee_id\": \"MKT001\", \"quarter\": \"Q4\"},\n", + " ],\n", + " },\n", + " {\n", + " \"name\": \"get_custom_budget\",\n", + " \"description\": 'Get the custom quarterly travel budget for a specific employee. Most employees have a standard $5,000 quarterly travel budget. However, some employees have custom budget exceptions based on their role requirements. This function checks if a specific employee has a custom budget assigned.\\n\\nRETURN FORMAT: Returns a JSON string containing a SINGLE OBJECT (not an array). Parse with json.loads() to get a dict. Example: {\"user_id\": \"ENG001\", \"has_custom_budget\": false, \"travel_budget\": 5000, \"reason\": \"Standard\", \"currency\": \"USD\"}',\n", + " \"input_schema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"user_id\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The unique employee identifier\",\n", + " }\n", + " },\n", + " \"required\": [\"user_id\"],\n", + " },\n", + " \"input_examples\": [\n", + " {\"user_id\": \"ENG001\"},\n", + " {\"user_id\": \"SAL002\"},\n", + " {\"user_id\": \"MKT001\"},\n", + " ],\n", + " },\n", + "]\n", + "\n", + "tool_functions = {\n", + " \"get_team_members\": get_team_members,\n", + " \"get_expenses\": get_expenses,\n", + " \"get_custom_budget\": get_custom_budget,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "ae7a7b68", + "metadata": {}, + "source": [ + "## Traditional Tool Calling (Baseline)\n", + "\n", + "In this first example, we'll use traditional tool calling to establish our baseline.\n", + "\n", + "We'll call the `messages.create` API with our initial query. When the model stops with a `tool_use` reason, we will execute the tool as requested, and then add the output from the tool to the messages and call the model again." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "dff83920", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "\n", + "from anthropic.types import TextBlock, ToolUseBlock\n", + "from anthropic.types.beta import (\n", + " BetaMessageParam as MessageParam,\n", + ")\n", + "from anthropic.types.beta import (\n", + " BetaTextBlock,\n", + " BetaToolUseBlock,\n", + ")\n", + "\n", + "messages: list[MessageParam] = []\n", + "\n", + "\n", + "def run_agent_without_ptc(user_message):\n", + " \"\"\"Run agent using traditional tool calling\"\"\"\n", + " messages.append({\"role\": \"user\", \"content\": user_message})\n", + " total_tokens = 0\n", + " start_time = time.time()\n", + " api_counter = 0\n", + "\n", + " while True:\n", + " response = client.beta.messages.create(\n", + " model=MODEL,\n", + " max_tokens=4000,\n", + " tools=tools,\n", + " messages=messages,\n", + " betas=[\"advanced-tool-use-2025-11-20\"],\n", + " )\n", + "\n", + " api_counter += 1\n", + "\n", + " # Track token usage\n", + " total_tokens += response.usage.input_tokens + response.usage.output_tokens\n", + " viz.capture(response)\n", + " if response.stop_reason == \"end_turn\":\n", + " # Extract the first text block from the response\n", + " final_response = next(\n", + " (\n", + " block.text\n", + " for block in response.content\n", + " if isinstance(block, (BetaTextBlock, TextBlock))\n", + " ),\n", + " None,\n", + " )\n", + " elapsed_time = time.time() - start_time\n", + " return final_response, messages, total_tokens, elapsed_time, api_counter\n", + "\n", + " # Process tool calls\n", + " if response.stop_reason == \"tool_use\":\n", + " # First, add the assistant's response to messages\n", + " messages.append({\"role\": \"assistant\", \"content\": response.content})\n", + "\n", + " # Collect all tool results\n", + " tool_results = []\n", + "\n", + " for block in response.content:\n", + " if isinstance(block, (BetaToolUseBlock, ToolUseBlock)):\n", + " tool_name = block.name\n", + " tool_input = block.input\n", + " tool_use_id = block.id\n", + "\n", + " result = tool_functions[tool_name](**tool_input)\n", + "\n", + " content = str(result)\n", + "\n", + " tool_result = {\n", + " \"type\": \"tool_result\",\n", + " \"tool_use_id\": tool_use_id,\n", + " \"content\": content,\n", + " }\n", + " tool_results.append(tool_result)\n", + "\n", + " # Append all tool results at once after collecting them\n", + " messages.append({\"role\": \"user\", \"content\": tool_results})\n", + "\n", + " else:\n", + " print(f\"\\nUnexpected stop reason: {response.stop_reason}\")\n", + " elapsed_time = time.time() - start_time\n", + "\n", + " final_response = next(\n", + " (\n", + " block.text\n", + " for block in response.content\n", + " if isinstance(block, (BetaTextBlock, TextBlock))\n", + " ),\n", + " f\"Stopped with reason: {response.stop_reason}\",\n", + " )\n", + " return final_response, messages, total_tokens, elapsed_time, api_counter" + ] + }, + { + "cell_type": "markdown", + "id": "db2d30d4", + "metadata": {}, + "source": [ + "Our initial query to the model provides some instructions to help guide the model. For brevity, we've asked the model to only call each tool once. For deeper investigations, the model may wish to look into multiple systems or time spans." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6d4bb83a", + "metadata": {}, + "outputs": [], + "source": [ + "query = \"Which engineering team members exceeded their Q3 travel budget? Standard quarterly travel budget is $5,000. However, some employees have custom budget limits. For anyone who exceeded the $5,000 standard budget, check if they have a custom budget exception. If they do, use that custom limit instead to determine if they truly exceeded their budget.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "ac08a17f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
╭────────────────────────────────────────────── Claude API Response ──────────────────────────────────────────────╮\n",
+       " Claude Message (assistant)  tokens: 1,859 in • 85 out • 1,944 total                                            \n",
+       " ├── Model: claude-sonnet-4-5-20250929                                                                           \n",
+       " ├── Stop Reason: tool_use                                                                                       \n",
+       " └── Content (2 blocks)                                                                                          \n",
+       "     ├── Block 1                                                                                                 \n",
+       "     │   └── Text                                                                                                \n",
+       "     │       └── I'll help you identify which engineering team members exceeded their Q3 travel budget. Let me   \n",
+       "start by getting the list of engineering team members.                                          \n",
+       "     └── Block 2                                                                                                 \n",
+       "         └── Tool Use: get_team_members                                                                          \n",
+       "             ├── ID: toolu_01LuouuJYp1sSvBe2Du7EG7v                                                              \n",
+       "             ├── Caller: model (direct)                                                                          \n",
+       "             └── Input:                                                                                          \n",
+       "                 └── {                                                                                           \n",
+       "                       \"department\": \"engineering\"                                                               \n",
+       "                     }                                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[36m╭─\u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[1;36mClaude API Response\u001b[0m\u001b[36m \u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[1;36mClaude Message\u001b[0m (\u001b[32massistant\u001b[0m) \u001b[2;37m│\u001b[0m \u001b[35mtokens:\u001b[0m \u001b[36m1,859\u001b[0m in • \u001b[32m85\u001b[0m out • \u001b[33m1,944\u001b[0m total \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mModel:\u001b[0m claude-sonnet-4-5-20250929 \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mStop Reason:\u001b[0m tool_use \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[1;37mContent\u001b[0m (2 blocks) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 1\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[36mText\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[37mI'll help you identify which engineering team members exceeded their Q3 travel budget. Let me \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37mstart by getting the list of engineering team members.\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[2;37mBlock 2\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_team_members\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mID:\u001b[0m toolu_01LuouuJYp1sSvBe2Du7EG7v \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mCaller:\u001b[0m model (direct) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"department\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"engineering\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭────────────────────────────── Claude API Response ───────────────────────────────╮\n",
+       " Claude Message (assistant)  tokens: 2,473 in • 497 out • 2,970 total            \n",
+       " ├── Model: claude-sonnet-4-5-20250929                                            \n",
+       " ├── Stop Reason: tool_use                                                        \n",
+       " └── Content (9 blocks)                                                           \n",
+       "     ├── Block 1                                                                  \n",
+       "     │   └── Text                                                                 \n",
+       "     │       └── Now let me get the Q3 expenses for all engineering team members: \n",
+       "     ├── Block 2                                                                  \n",
+       "     │   └── Tool Use: get_expenses                                               \n",
+       "     │       ├── ID: toolu_01Wu8LLTT2sKTTqpVwGT65Lj                               \n",
+       "     │       ├── Caller: model (direct)                                           \n",
+       "     │       └── Input:                                                           \n",
+       "     │           └── {                                                            \n",
+       "  \"employee_id\": \"ENG001\",                                   \n",
+       "  \"quarter\": \"Q3\"                                            \n",
+       "}                                                            \n",
+       "     ├── Block 3                                                                  \n",
+       "     │   └── Tool Use: get_expenses                                               \n",
+       "     │       ├── ID: toolu_01KzjQ5mQJa9ocWjCGzYkD9F                               \n",
+       "     │       ├── Caller: model (direct)                                           \n",
+       "     │       └── Input:                                                           \n",
+       "     │           └── {                                                            \n",
+       "  \"employee_id\": \"ENG002\",                                   \n",
+       "  \"quarter\": \"Q3\"                                            \n",
+       "}                                                            \n",
+       "     ├── Block 4                                                                  \n",
+       "     │   └── Tool Use: get_expenses                                               \n",
+       "     │       ├── ID: toolu_01RjjhZTg9JsKXE5E9S6Foho                               \n",
+       "     │       ├── Caller: model (direct)                                           \n",
+       "     │       └── Input:                                                           \n",
+       "     │           └── {                                                            \n",
+       "  \"employee_id\": \"ENG003\",                                   \n",
+       "  \"quarter\": \"Q3\"                                            \n",
+       "}                                                            \n",
+       "     ├── Block 5                                                                  \n",
+       "     │   └── Tool Use: get_expenses                                               \n",
+       "     │       ├── ID: toolu_013xqpxpfc2N9rP5W5uMLAo9                               \n",
+       "     │       ├── Caller: model (direct)                                           \n",
+       "     │       └── Input:                                                           \n",
+       "     │           └── {                                                            \n",
+       "  \"employee_id\": \"ENG004\",                                   \n",
+       "  \"quarter\": \"Q3\"                                            \n",
+       "}                                                            \n",
+       "     ├── Block 6                                                                  \n",
+       "     │   └── Tool Use: get_expenses                                               \n",
+       "     │       ├── ID: toolu_019zfzG6Wox8iDqy1dUXiH3t                               \n",
+       "     │       ├── Caller: model (direct)                                           \n",
+       "     │       └── Input:                                                           \n",
+       "     │           └── {                                                            \n",
+       "  \"employee_id\": \"ENG005\",                                   \n",
+       "  \"quarter\": \"Q3\"                                            \n",
+       "}                                                            \n",
+       "     ├── Block 7                                                                  \n",
+       "     │   └── Tool Use: get_expenses                                               \n",
+       "     │       ├── ID: toolu_01RxfTz11tzvbVE7oEtqHaVB                               \n",
+       "     │       ├── Caller: model (direct)                                           \n",
+       "     │       └── Input:                                                           \n",
+       "     │           └── {                                                            \n",
+       "  \"employee_id\": \"ENG006\",                                   \n",
+       "  \"quarter\": \"Q3\"                                            \n",
+       "}                                                            \n",
+       "     ├── Block 8                                                                  \n",
+       "     │   └── Tool Use: get_expenses                                               \n",
+       "     │       ├── ID: toolu_01FsFEtK1gTEPxg56eVrhhf6                               \n",
+       "     │       ├── Caller: model (direct)                                           \n",
+       "     │       └── Input:                                                           \n",
+       "     │           └── {                                                            \n",
+       "  \"employee_id\": \"ENG007\",                                   \n",
+       "  \"quarter\": \"Q3\"                                            \n",
+       "}                                                            \n",
+       "     └── Block 9                                                                  \n",
+       "         └── Tool Use: get_expenses                                               \n",
+       "             ├── ID: toolu_01Ctq9dZbvzaVSLSZe86MTzb                               \n",
+       "             ├── Caller: model (direct)                                           \n",
+       "             └── Input:                                                           \n",
+       "                 └── {                                                            \n",
+       "                       \"employee_id\": \"ENG008\",                                   \n",
+       "                       \"quarter\": \"Q3\"                                            \n",
+       "                     }                                                            \n",
+       "╰──────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[36m╭─\u001b[0m\u001b[36m─────────────────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[1;36mClaude API Response\u001b[0m\u001b[36m \u001b[0m\u001b[36m──────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[1;36mClaude Message\u001b[0m (\u001b[32massistant\u001b[0m) \u001b[2;37m│\u001b[0m \u001b[35mtokens:\u001b[0m \u001b[36m2,473\u001b[0m in • \u001b[32m497\u001b[0m out • \u001b[33m2,970\u001b[0m total \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mModel:\u001b[0m claude-sonnet-4-5-20250929 \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mStop Reason:\u001b[0m tool_use \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[1;37mContent\u001b[0m (9 blocks) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 1\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[36mText\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[37mNow let me get the Q3 expenses for all engineering team members:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 2\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_expenses\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01Wu8LLTT2sKTTqpVwGT65Lj \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m model (direct) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"employee_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG001\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"quarter\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"Q3\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 3\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_expenses\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01KzjQ5mQJa9ocWjCGzYkD9F \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m model (direct) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"employee_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG002\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"quarter\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"Q3\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 4\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_expenses\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01RjjhZTg9JsKXE5E9S6Foho \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m model (direct) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"employee_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG003\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"quarter\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"Q3\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 5\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_expenses\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_013xqpxpfc2N9rP5W5uMLAo9 \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m model (direct) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"employee_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG004\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"quarter\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"Q3\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 6\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_expenses\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_019zfzG6Wox8iDqy1dUXiH3t \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m model (direct) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"employee_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG005\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"quarter\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"Q3\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 7\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_expenses\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01RxfTz11tzvbVE7oEtqHaVB \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m model (direct) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"employee_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG006\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"quarter\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"Q3\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 8\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_expenses\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01FsFEtK1gTEPxg56eVrhhf6 \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m model (direct) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"employee_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG007\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"quarter\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"Q3\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[2;37mBlock 9\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_expenses\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mID:\u001b[0m toolu_01Ctq9dZbvzaVSLSZe86MTzb \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mCaller:\u001b[0m model (direct) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"employee_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG008\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"quarter\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"Q3\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m╰──────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭────────────────────────────────────────────── Claude API Response ──────────────────────────────────────────────╮\n",
+       " Claude Message (assistant)  tokens: 51,744 in • 290 out • 52,034 total                                         \n",
+       " ├── Model: claude-sonnet-4-5-20250929                                                                           \n",
+       " ├── Stop Reason: tool_use                                                                                       \n",
+       " └── Content (7 blocks)                                                                                          \n",
+       "     ├── Block 1                                                                                                 \n",
+       "     │   └── Text                                                                                                \n",
+       "     │       └── Now let me calculate the approved travel expenses for each engineer and identify who exceeded   \n",
+       "$5,000:                                                                                         \n",
+       "     ├── Block 2                                                                                                 \n",
+       "     │   └── Tool Use: get_custom_budget                                                                         \n",
+       "     │       ├── ID: toolu_013oegKwjvToLwEW1daDD8av                                                              \n",
+       "     │       ├── Caller: model (direct)                                                                          \n",
+       "     │       └── Input:                                                                                          \n",
+       "     │           └── {                                                                                           \n",
+       "  \"user_id\": \"ENG001\"                                                                       \n",
+       "}                                                                                           \n",
+       "     ├── Block 3                                                                                                 \n",
+       "     │   └── Tool Use: get_custom_budget                                                                         \n",
+       "     │       ├── ID: toolu_0162W4Ycr9FcVVED65exjAj4                                                              \n",
+       "     │       ├── Caller: model (direct)                                                                          \n",
+       "     │       └── Input:                                                                                          \n",
+       "     │           └── {                                                                                           \n",
+       "  \"user_id\": \"ENG003\"                                                                       \n",
+       "}                                                                                           \n",
+       "     ├── Block 4                                                                                                 \n",
+       "     │   └── Tool Use: get_custom_budget                                                                         \n",
+       "     │       ├── ID: toolu_01JcTX5rnwFxA99Am33gXmh6                                                              \n",
+       "     │       ├── Caller: model (direct)                                                                          \n",
+       "     │       └── Input:                                                                                          \n",
+       "     │           └── {                                                                                           \n",
+       "  \"user_id\": \"ENG005\"                                                                       \n",
+       "}                                                                                           \n",
+       "     ├── Block 5                                                                                                 \n",
+       "     │   └── Tool Use: get_custom_budget                                                                         \n",
+       "     │       ├── ID: toolu_01QwNJz1wGeV5VeZoCd4ByER                                                              \n",
+       "     │       ├── Caller: model (direct)                                                                          \n",
+       "     │       └── Input:                                                                                          \n",
+       "     │           └── {                                                                                           \n",
+       "  \"user_id\": \"ENG006\"                                                                       \n",
+       "}                                                                                           \n",
+       "     ├── Block 6                                                                                                 \n",
+       "     │   └── Tool Use: get_custom_budget                                                                         \n",
+       "     │       ├── ID: toolu_01KoJ4gzfiu1TPccLJB86Wiq                                                              \n",
+       "     │       ├── Caller: model (direct)                                                                          \n",
+       "     │       └── Input:                                                                                          \n",
+       "     │           └── {                                                                                           \n",
+       "  \"user_id\": \"ENG007\"                                                                       \n",
+       "}                                                                                           \n",
+       "     └── Block 7                                                                                                 \n",
+       "         └── Tool Use: get_custom_budget                                                                         \n",
+       "             ├── ID: toolu_01MxeFPzHot9aE5fPuniFkui                                                              \n",
+       "             ├── Caller: model (direct)                                                                          \n",
+       "             └── Input:                                                                                          \n",
+       "                 └── {                                                                                           \n",
+       "                       \"user_id\": \"ENG008\"                                                                       \n",
+       "                     }                                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[36m╭─\u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[1;36mClaude API Response\u001b[0m\u001b[36m \u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[1;36mClaude Message\u001b[0m (\u001b[32massistant\u001b[0m) \u001b[2;37m│\u001b[0m \u001b[35mtokens:\u001b[0m \u001b[36m51,744\u001b[0m in • \u001b[32m290\u001b[0m out • \u001b[33m52,034\u001b[0m total \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mModel:\u001b[0m claude-sonnet-4-5-20250929 \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mStop Reason:\u001b[0m tool_use \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[1;37mContent\u001b[0m (7 blocks) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 1\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[36mText\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[37mNow let me calculate the approved travel expenses for each engineer and identify who exceeded \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37m$5,000:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 2\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_custom_budget\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_013oegKwjvToLwEW1daDD8av \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m model (direct) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"user_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG001\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 3\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_custom_budget\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_0162W4Ycr9FcVVED65exjAj4 \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m model (direct) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"user_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG003\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 4\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_custom_budget\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01JcTX5rnwFxA99Am33gXmh6 \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m model (direct) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"user_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG005\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 5\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_custom_budget\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01QwNJz1wGeV5VeZoCd4ByER \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m model (direct) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"user_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG006\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 6\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_custom_budget\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01KoJ4gzfiu1TPccLJB86Wiq \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m model (direct) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"user_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG007\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[2;37mBlock 7\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_custom_budget\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mID:\u001b[0m toolu_01MxeFPzHot9aE5fPuniFkui \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mCaller:\u001b[0m model (direct) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"user_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG008\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭────────────────────────────────────────────── Claude API Response ──────────────────────────────────────────────╮\n",
+       " Claude Message (assistant)  tokens: 52,533 in • 992 out • 53,525 total                                         \n",
+       " ├── Model: claude-sonnet-4-5-20250929                                                                           \n",
+       " ├── Stop Reason: end_turn                                                                                       \n",
+       " └── Content (1 blocks)                                                                                          \n",
+       "     └── Block 1                                                                                                 \n",
+       "         └── Text                                                                                                \n",
+       "             └── Now let me analyze the data. I'll calculate the approved travel expenses for each engineer:     \n",
+       "                                                                                                                 \n",
+       "                 **Analysis of Q3 Travel Expenses:**                                                             \n",
+       "                                                                                                                 \n",
+       "                 **ENG001 - Alice Chen (Senior Software Engineer)**                                              \n",
+       "                 - Approved travel expenses: $1,161.04 + $18.63 + $13.21 + $36.55 + $1,440.42 + $166.46 + $48.43 \n",
+       "                 + $1,124.56 + $1,245.90 + $1,498.42 = **$6,753.62**                                             \n",
+       "                 - Budget: $5,000 (Standard)                                                                     \n",
+       "                 - **EXCEEDED by $1,753.62** ❌                                                                  \n",
+       "                                                                                                                 \n",
+       "                 **ENG002 - Bob Martinez (Staff Engineer)**                                                      \n",
+       "                 - Approved travel expenses: $180.16 + $10.07 + $20.76 = **$210.99**                             \n",
+       "                 - Budget: $5,000 (Standard)                                                                     \n",
+       "                 - Under budget ✓                                                                                \n",
+       "                                                                                                                 \n",
+       "                 **ENG003 - Carol White (Software Engineer)**                                                    \n",
+       "                 - Approved travel expenses: $24.75 + $424.74 + $1,397.17 + $1,026.12 + $1,288.36 + $1,128.90 +  \n",
+       "                 $1,148.42 + $45.03 = **$6,483.49**                                                              \n",
+       "                 - Budget: $5,000 (Standard)                                                                     \n",
+       "                 - **EXCEEDED by $1,483.49** ❌                                                                  \n",
+       "                                                                                                                 \n",
+       "                 **ENG004 - David Kim (Principal Engineer)**                                                     \n",
+       "                 - Approved travel expenses: $21.68 + $46.12 + $1,008.68 + $46.43 = **$1,122.91**                \n",
+       "                 - Budget: $5,000 (Standard)                                                                     \n",
+       "                 - Under budget ✓                                                                                \n",
+       "                                                                                                                 \n",
+       "                 **ENG005 - Emma Johnson (Junior Software Engineer)                                              \n",
+       "                 ... (truncated)                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[36m╭─\u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[1;36mClaude API Response\u001b[0m\u001b[36m \u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[1;36mClaude Message\u001b[0m (\u001b[32massistant\u001b[0m) \u001b[2;37m│\u001b[0m \u001b[35mtokens:\u001b[0m \u001b[36m52,533\u001b[0m in • \u001b[32m992\u001b[0m out • \u001b[33m53,525\u001b[0m total \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mModel:\u001b[0m claude-sonnet-4-5-20250929 \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mStop Reason:\u001b[0m end_turn \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[1;37mContent\u001b[0m (1 blocks) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[2;37mBlock 1\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[36mText\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[37mNow let me analyze the data. I'll calculate the approved travel expenses for each engineer:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m**Analysis of Q3 Travel Expenses:**\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m**ENG001 - Alice Chen (Senior Software Engineer)**\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m- Approved travel expenses: $1,161.04 + $18.63 + $13.21 + $36.55 + $1,440.42 + $166.46 + $48.43\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m+ $1,124.56 + $1,245.90 + $1,498.42 = **$6,753.62**\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m- Budget: $5,000 (Standard)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m- **EXCEEDED by $1,753.62** ❌\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m**ENG002 - Bob Martinez (Staff Engineer)**\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m- Approved travel expenses: $180.16 + $10.07 + $20.76 = **$210.99**\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m- Budget: $5,000 (Standard)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m- Under budget ✓\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m**ENG003 - Carol White (Software Engineer)**\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m- Approved travel expenses: $24.75 + $424.74 + $1,397.17 + $1,026.12 + $1,288.36 + $1,128.90 + \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m$1,148.42 + $45.03 = **$6,483.49**\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m- Budget: $5,000 (Standard)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m- **EXCEEDED by $1,483.49** ❌\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m**ENG004 - David Kim (Principal Engineer)**\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m- Approved travel expenses: $21.68 + $46.12 + $1,008.68 + $46.43 = **$1,122.91**\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m- Budget: $5,000 (Standard)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m- Under budget ✓\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m**ENG005 - Emma Johnson (Junior Software Engineer)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m... (truncated)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Result: Now let me analyze the data. I'll calculate the approved travel expenses for each engineer:\n", + "\n", + "**Analysis of Q3 Travel Expenses:**\n", + "\n", + "**ENG001 - Alice Chen (Senior Software Engineer)**\n", + "- Approved travel expenses: $1,161.04 + $18.63 + $13.21 + $36.55 + $1,440.42 + $166.46 + $48.43 + $1,124.56 + $1,245.90 + $1,498.42 = **$6,753.62**\n", + "- Budget: $5,000 (Standard)\n", + "- **EXCEEDED by $1,753.62** ❌\n", + "\n", + "**ENG002 - Bob Martinez (Staff Engineer)**\n", + "- Approved travel expenses: $180.16 + $10.07 + $20.76 = **$210.99**\n", + "- Budget: $5,000 (Standard)\n", + "- Under budget ✓\n", + "\n", + "**ENG003 - Carol White (Software Engineer)**\n", + "- Approved travel expenses: $24.75 + $424.74 + $1,397.17 + $1,026.12 + $1,288.36 + $1,128.90 + $1,148.42 + $45.03 = **$6,483.49**\n", + "- Budget: $5,000 (Standard)\n", + "- **EXCEEDED by $1,483.49** ❌\n", + "\n", + "**ENG004 - David Kim (Principal Engineer)**\n", + "- Approved travel expenses: $21.68 + $46.12 + $1,008.68 + $46.43 = **$1,122.91**\n", + "- Budget: $5,000 (Standard)\n", + "- Under budget ✓\n", + "\n", + "**ENG005 - Emma Johnson (Junior Software Engineer)**\n", + "- Approved travel expenses: $450.00 + $1,376.36 + $1,164.49 + $151.55 + $1,253.88 = **$4,396.28**\n", + "- Budget: $5,000 (Standard)\n", + "- Under budget ✓\n", + "\n", + "**ENG006 - Frank Liu (Senior Software Engineer)**\n", + "- Approved travel expenses: $596.48 + $1,018.71 + $1,193.82 + $159.08 + $1,112.11 + $24.97 = **$4,105.17**\n", + "- Budget: $5,000 (Standard)\n", + "- Under budget ✓\n", + "\n", + "**ENG007 - Grace Taylor (Software Engineer)**\n", + "- Approved travel expenses: $1,476.63 + $39.85 + $1,220.19 + $189.16 + $1,032.52 + $1,331.00 = **$5,289.35**\n", + "- Budget: $5,000 (Standard)\n", + "- **EXCEEDED by $289.35** ❌\n", + "\n", + "**ENG008 - Henry Park (Staff Engineer)**\n", + "- Approved travel expenses: $15.63 + $166.05 + $1,018.94 + $1,224.34 + $1,120.32 + $1,345.90 = **$4,891.18**\n", + "- Budget: $5,000 (Standard)\n", + "- Under budget ✓\n", + "\n", + "---\n", + "\n", + "## Summary: Engineering Team Members Who Exceeded Their Q3 Travel Budget\n", + "\n", + "**3 team members exceeded their quarterly travel budget:**\n", + "\n", + "1. **Alice Chen (ENG001)** - Senior Software Engineer\n", + " - Travel expenses: **$6,753.62**\n", + " - Budget: $5,000\n", + " - **Over budget by $1,753.62 (35% over)**\n", + "\n", + "2. **Carol White (ENG003)** - Software Engineer\n", + " - Travel expenses: **$6,483.49**\n", + " - Budget: $5,000\n", + " - **Over budget by $1,483.49 (30% over)**\n", + "\n", + "3. **Grace Taylor (ENG007)** - Software Engineer\n", + " - Travel expenses: **$5,289.35**\n", + " - Budget: $5,000\n", + " - **Over budget by $289.35 (6% over)**\n", + "\n", + "All three employees have the standard $5,000 quarterly travel budget with no custom exceptions.\n", + "API calls made: 4\n", + "Total tokens used: 110,473\n", + "Total time taken: 35.38s\n" + ] + } + ], + "source": [ + "# Run the agent\n", + "result, conversation, total_tokens, elapsed_time, api_count_without_ptc = run_agent_without_ptc(\n", + " query\n", + ")\n", + "\n", + "print(f\"Result: {result}\")\n", + "print(f\"API calls made: {api_count_without_ptc}\")\n", + "print(f\"Total tokens used: {total_tokens:,}\")\n", + "print(f\"Total time taken: {elapsed_time:.2f}s\")" + ] + }, + { + "cell_type": "markdown", + "id": "6a3fedb1", + "metadata": {}, + "source": [ + "Great! We can see that Claude was able to use the available tools successfully to identify which team members exceeded their travel budgets. However, we can also see that we used a lot of tokens to accomplish this task. Claude had to ingest all the expense line items through its context window—potentially 100+ records per employee, each with extensive metadata including receipt URLs, approval chains, merchant information, and more—in order to parse them, sum up the totals by category, and compare against budget limits.\n", + "\n", + "Additionally, the traditional tool calling approach requires multiple sequential round trips: first fetching team members, then expenses for each person, then checking custom budgets for those who exceeded the standard limit. Each round trip adds latency, and all the rich metadata from expense records flows through the model's context.\n", + "\n", + "Let's see if we can use PTC to improve performance by allowing Claude to write code that processes these large datasets in the code execution environment instead." + ] + }, + { + "cell_type": "markdown", + "id": "f9c2500e", + "metadata": {}, + "source": [ + "To enable PTC on tools, we must first add the `allowed_callers` field to any tool that should be callable via code execution.\n", + "\n", + "**Key points to consider**\n", + "\n", + "- Tools without allowed_callers default to model-only invocation\n", + "- Tools can be invoked by both the model AND code execution by including multiple callers: `[\"direct\", \"code_execution_20250825\"]`\n", + "- Only opt in tools that are safe for programmatic/repeated execution.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "yg1hozsmgz9", + "metadata": {}, + "outputs": [], + "source": [ + "import copy\n", + "\n", + "ptc_tools = copy.deepcopy(tools)\n", + "for tool in ptc_tools:\n", + " tool[\"allowed_callers\"] = [\"code_execution_20250825\"] # type: ignore\n", + "\n", + "\n", + "# Add the code execution tool\n", + "ptc_tools.append(\n", + " {\n", + " \"type\": \"code_execution_20250825\", # type: ignore\n", + " \"name\": \"code_execution\",\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "c9d1c138", + "metadata": {}, + "source": [ + "Now that we've updated our tool definitions to allow programmatic tool calling, we can run our agent with PTC. In order to do so, we've had to make a few changes to our function. We must use the `beta` messages API. \n", + "\n", + "1. We've added `\"advanced-tool-use-2025-11-20\"` to betas. \n", + "2. We pass in the `container_id` if it is defined with our request. This is only necessary for stateful workflows like ours. In single-turn workflows this is not required.\n", + "3. We can check the `caller` field in the `tool_use` block to determine if this tool call is from a direct model invocation or from programmatic invocation. \n", + "\n", + "Note that in either case, we send our tool results via the Claude API, however only `direct` invocations will be \"seen\" by the model. `code_execution_20250825` types will only be seen my the code execution container. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "dq3gj54mlv", + "metadata": {}, + "outputs": [], + "source": [ + "messages = []\n", + "\n", + "\n", + "def run_agent_with_ptc(user_message):\n", + " \"\"\"Run agent using PTC\"\"\"\n", + " messages.append({\"role\": \"user\", \"content\": user_message})\n", + " total_tokens = 0\n", + " start_time = time.time()\n", + " container_id = None\n", + " api_counter = 0\n", + "\n", + " while True:\n", + " # Build request with PTC beta headers\n", + " request_params = {\n", + " \"model\": MODEL,\n", + " \"max_tokens\": 4000,\n", + " \"tools\": ptc_tools,\n", + " \"messages\": messages,\n", + " }\n", + "\n", + " response = client.beta.messages.create(\n", + " **request_params,\n", + " betas=[\n", + " \"advanced-tool-use-2025-11-20\",\n", + " ],\n", + " extra_body={\"container\": container_id} if container_id else None,\n", + " )\n", + " viz.capture(response)\n", + " api_counter += 1\n", + "\n", + " # Track container for stateful execution\n", + " if hasattr(response, \"container\") and response.container:\n", + " container_id = response.container.id\n", + " print(f\"\\n[Container] ID: {container_id}\")\n", + " if hasattr(response.container, \"expires_at\"):\n", + " # If the container has expired, we would need to restart our workflow. In our case, it completes before expiration.\n", + " print(f\"[Container] Expires at: {response.container.expires_at}\")\n", + "\n", + " # Track token usage\n", + " total_tokens += response.usage.input_tokens + response.usage.output_tokens\n", + "\n", + " if response.stop_reason == \"end_turn\":\n", + " # Extract the first text block from the response\n", + " final_response = next(\n", + " (block.text for block in response.content if isinstance(block, BetaTextBlock)),\n", + " None,\n", + " )\n", + " elapsed_time = time.time() - start_time\n", + " return final_response, messages, total_tokens, elapsed_time, api_counter\n", + "\n", + " # As before, we process tool calls\n", + " if response.stop_reason == \"tool_use\":\n", + " # First, add the assistant's response to messages\n", + " messages.append({\"role\": \"assistant\", \"content\": response.content})\n", + "\n", + " # Collect all tool results\n", + " tool_results = []\n", + "\n", + " for block in response.content:\n", + " if isinstance(block, BetaToolUseBlock):\n", + " tool_name = block.name\n", + " tool_input = block.input\n", + " tool_use_id = block.id\n", + "\n", + " # We can use caller type to understand how the tool was invoked\n", + " caller_type = block.caller[\"type\"] # type: ignore\n", + "\n", + " if caller_type == \"code_execution_20250825\":\n", + " print(f\"[PTC] Tool called from code execution environment: {tool_name}\")\n", + "\n", + " elif caller_type == \"direct\":\n", + " print(f\"[Direct] Tool called by model: {tool_name}\")\n", + "\n", + " result = tool_functions[tool_name](**tool_input)\n", + "\n", + " # Format result as proper content for the API\n", + " if isinstance(result, list) and result and isinstance(result[0], str):\n", + " content = \"\\n\".join(result)\n", + " elif isinstance(result, (dict, list)):\n", + " content = json.dumps(result)\n", + " else:\n", + " content = str(result)\n", + "\n", + " tool_results.append(\n", + " {\n", + " \"type\": \"tool_result\",\n", + " \"tool_use_id\": tool_use_id,\n", + " \"content\": content,\n", + " }\n", + " )\n", + "\n", + " messages.append({\"role\": \"user\", \"content\": tool_results})\n", + "\n", + " else:\n", + " print(f\"\\nUnexpected stop reason: {response.stop_reason}\")\n", + " elapsed_time = time.time() - start_time\n", + "\n", + " final_response = next(\n", + " (block.text for block in response.content if isinstance(block, BetaTextBlock)),\n", + " f\"Stopped with reason: {response.stop_reason}\",\n", + " )\n", + " return final_response, messages, total_tokens, elapsed_time, api_counter" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "640d2e02", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
╭────────────────────────────────────────────── Claude API Response ──────────────────────────────────────────────╮\n",
+       " Claude Message (assistant)  tokens: 4,134 in • 539 out • 4,673 total                                           \n",
+       " ├── Model: claude-sonnet-4-5-20250929                                                                           \n",
+       " ├── Stop Reason: tool_use                                                                                       \n",
+       " └── Content (3 blocks)                                                                                          \n",
+       "     ├── Block 1                                                                                                 \n",
+       "     │   └── Text                                                                                                \n",
+       "     │       └── I'll help you identify which engineering team members exceeded their Q3 travel budget. Let me   \n",
+       "start by getting the engineering team members and their expenses.                               \n",
+       "     ├── Block 2                                                                                                 \n",
+       "     │   └── Server Tool Use                                                                                     \n",
+       "     │       ├── ID: srvtoolu_015mWPqaFni4B313UieCxbny                                                           \n",
+       "     │       ├── Caller: direct                                                                                  \n",
+       "     │       └── Code:                                                                                           \n",
+       "     │           └──    1                                                                                        \n",
+       "   2 import asyncio                                                                         \n",
+       "   3 import json                                                                            \n",
+       "   4                                                                                        \n",
+       "   5 async def main():                                                                      \n",
+       "   6     # First, get all engineering team members                                          \n",
+       "   7     team_members_json = await get_team_members({'department': 'engineering'})          \n",
+       "   8     team_members = json.loads(team_members_json)                                       \n",
+       "   9                                                                                        \n",
+       "  10     print(f\"Found {len(team_members)} engineering team members\")                       \n",
+       "  11                                                                                        \n",
+       "  12     # Get Q3 expenses for all team members in parallel                                 \n",
+       "  13     expense_tasks = [                                                                  \n",
+       "  14         get_expenses({'employee_id': member['id'], 'quarter': 'Q3'})                   \n",
+       "  15         for member in team_members                                                     \n",
+       "  16     ]                                                                                  \n",
+       "  17                                                                                        \n",
+       "  18     expenses_results = await asyncio.gather(*expense_tasks)                            \n",
+       "  19                                                                                        \n",
+       "  20     # Calculate travel expenses for each member                                        \n",
+       "  21     travel_spending = {}                                                               \n",
+       "  22     for i, member in enumerate(team_members):                                          \n",
+       "  23         expenses = json.loads(expenses_results[i])                                     \n",
+       "  24         # Only count approved expenses in travel category                              \n",
+       "  25         travel_total = sum(                                                            \n",
+       "  26             expense['amount']                                                          \n",
+       "  27             for expense in expenses                                                    \n",
+       "  28             if expense['category'] == 'travel' and expense['status'] == 'approved'     \n",
+       "  29         )                                                                              \n",
+       "  30         travel_spending[member[                                                        \n",
+       "  31 ... (truncated)                                                                        \n",
+       "     └── Block 3                                                                                                 \n",
+       "         └── Tool Use: get_team_members                                                                          \n",
+       "             ├── ID: toolu_016EtCE7G5rH645G3gvjzrP6                                                              \n",
+       "             ├── Caller: code execution environment                                                              \n",
+       "             └── Input:                                                                                          \n",
+       "                 └── {                                                                                           \n",
+       "                       \"department\": \"engineering\"                                                               \n",
+       "                     }                                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[36m╭─\u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[1;36mClaude API Response\u001b[0m\u001b[36m \u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[1;36mClaude Message\u001b[0m (\u001b[32massistant\u001b[0m) \u001b[2;37m│\u001b[0m \u001b[35mtokens:\u001b[0m \u001b[36m4,134\u001b[0m in • \u001b[32m539\u001b[0m out • \u001b[33m4,673\u001b[0m total \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mModel:\u001b[0m claude-sonnet-4-5-20250929 \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mStop Reason:\u001b[0m tool_use \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[1;37mContent\u001b[0m (3 blocks) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 1\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[36mText\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[37mI'll help you identify which engineering team members exceeded their Q3 travel budget. Let me \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37mstart by getting the engineering team members and their expenses.\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 2\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mServer Tool Use\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m srvtoolu_015mWPqaFni4B313UieCxbny \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m direct \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mCode:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 1 \u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 2 \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34mimport\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34masyncio\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 3 \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34mimport\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mjson\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 4 \u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 5 \u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34masync\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34mdef\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;166;226;46;48;2;39;40;34mmain\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 6 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;149;144;119;48;2;39;40;34m# First, get all engineering team members\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 7 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mteam_members_json\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34mawait\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mget_team_members\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mdepartment\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mengineering\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 8 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mteam_members\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mjson\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mloads\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mteam_members_json\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 9 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m10 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mprint\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mf\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mFound \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m{\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mlen\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mteam_members\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m}\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m engineering team members\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m11 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m12 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;149;144;119;48;2;39;40;34m# Get Q3 expenses for all team members in parallel\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m13 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mexpense_tasks\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m14 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mget_expenses\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34memployee_id\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mmember\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mid\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mquarter\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mQ3\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m15 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34mfor\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mmember\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34min\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mteam_members\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m16 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m17 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m18 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mexpenses_results\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34mawait\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34masyncio\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mgather\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m*\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mexpense_tasks\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m19 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m20 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;149;144;119;48;2;39;40;34m# Calculate travel expenses for each member\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m21 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtravel_spending\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m22 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34mfor\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mi\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mmember\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34min\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34menumerate\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mteam_members\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m23 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mexpenses\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mjson\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mloads\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mexpenses_results\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mi\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m24 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;149;144;119;48;2;39;40;34m# Only count approved expenses in travel category\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m25 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtravel_total\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msum\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m26 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mexpense\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mamount\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m27 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34mfor\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mexpense\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34min\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mexpenses\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m28 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34mif\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mexpense\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcategory\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m==\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mtravel\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34mand\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mexpense\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mstatus\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m==\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mapproved\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m29 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m30 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtravel_spending\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mmember\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m31 \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtruncated\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[2;37mBlock 3\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_team_members\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mID:\u001b[0m toolu_016EtCE7G5rH645G3gvjzrP6 \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mCaller:\u001b[0m code execution environment \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"department\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"engineering\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "[Container] ID: container_011CVSAwq5J4vNPi3A4P2Rwh\n", + "[Container] Expires at: 2025-11-24 05:41:17.467494+00:00\n", + "[PTC] Tool called from code execution environment: get_team_members\n" + ] + }, + { + "data": { + "text/html": [ + "
╭──────────────────── Claude API Response ────────────────────╮\n",
+       " Claude Message (assistant)  tokens: 0 in • 0 out • 0 total \n",
+       " ├── Model: claude-sonnet-4-5-20250929                       \n",
+       " ├── Stop Reason: tool_use                                   \n",
+       " └── Content (8 blocks)                                      \n",
+       "     ├── Block 1                                             \n",
+       "     │   └── Tool Use: get_expenses                          \n",
+       "     │       ├── ID: toolu_01Nq2au3W69RmDFZaSdqe6u1          \n",
+       "     │       ├── Caller: code execution environment          \n",
+       "     │       └── Input:                                      \n",
+       "     │           └── {                                       \n",
+       "  \"employee_id\": \"ENG007\",              \n",
+       "  \"quarter\": \"Q3\"                       \n",
+       "}                                       \n",
+       "     ├── Block 2                                             \n",
+       "     │   └── Tool Use: get_expenses                          \n",
+       "     │       ├── ID: toolu_01YYw9cuTSXk7bu7P38qBz6P          \n",
+       "     │       ├── Caller: code execution environment          \n",
+       "     │       └── Input:                                      \n",
+       "     │           └── {                                       \n",
+       "  \"employee_id\": \"ENG005\",              \n",
+       "  \"quarter\": \"Q3\"                       \n",
+       "}                                       \n",
+       "     ├── Block 3                                             \n",
+       "     │   └── Tool Use: get_expenses                          \n",
+       "     │       ├── ID: toolu_01Fyxxe2KmVpVmw4jJL2CXSz          \n",
+       "     │       ├── Caller: code execution environment          \n",
+       "     │       └── Input:                                      \n",
+       "     │           └── {                                       \n",
+       "  \"employee_id\": \"ENG008\",              \n",
+       "  \"quarter\": \"Q3\"                       \n",
+       "}                                       \n",
+       "     ├── Block 4                                             \n",
+       "     │   └── Tool Use: get_expenses                          \n",
+       "     │       ├── ID: toolu_01J4ovDu2UJa9Se19vxKTa6y          \n",
+       "     │       ├── Caller: code execution environment          \n",
+       "     │       └── Input:                                      \n",
+       "     │           └── {                                       \n",
+       "  \"employee_id\": \"ENG006\",              \n",
+       "  \"quarter\": \"Q3\"                       \n",
+       "}                                       \n",
+       "     ├── Block 5                                             \n",
+       "     │   └── Tool Use: get_expenses                          \n",
+       "     │       ├── ID: toolu_01T24CrvQYA3LqGfZftCmueC          \n",
+       "     │       ├── Caller: code execution environment          \n",
+       "     │       └── Input:                                      \n",
+       "     │           └── {                                       \n",
+       "  \"employee_id\": \"ENG003\",              \n",
+       "  \"quarter\": \"Q3\"                       \n",
+       "}                                       \n",
+       "     ├── Block 6                                             \n",
+       "     │   └── Tool Use: get_expenses                          \n",
+       "     │       ├── ID: toolu_01HotYZN6sk3gMLkpdWXbdz4          \n",
+       "     │       ├── Caller: code execution environment          \n",
+       "     │       └── Input:                                      \n",
+       "     │           └── {                                       \n",
+       "  \"employee_id\": \"ENG004\",              \n",
+       "  \"quarter\": \"Q3\"                       \n",
+       "}                                       \n",
+       "     ├── Block 7                                             \n",
+       "     │   └── Tool Use: get_expenses                          \n",
+       "     │       ├── ID: toolu_01AxvEqi3AKqdnH44kGH1U6E          \n",
+       "     │       ├── Caller: code execution environment          \n",
+       "     │       └── Input:                                      \n",
+       "     │           └── {                                       \n",
+       "  \"employee_id\": \"ENG002\",              \n",
+       "  \"quarter\": \"Q3\"                       \n",
+       "}                                       \n",
+       "     └── Block 8                                             \n",
+       "         └── Tool Use: get_expenses                          \n",
+       "             ├── ID: toolu_01A4agznkK1jA4AyJo3H2jpg          \n",
+       "             ├── Caller: code execution environment          \n",
+       "             └── Input:                                      \n",
+       "                 └── {                                       \n",
+       "                       \"employee_id\": \"ENG001\",              \n",
+       "                       \"quarter\": \"Q3\"                       \n",
+       "                     }                                       \n",
+       "╰─────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[36m╭─\u001b[0m\u001b[36m───────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[1;36mClaude API Response\u001b[0m\u001b[36m \u001b[0m\u001b[36m───────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[1;36mClaude Message\u001b[0m (\u001b[32massistant\u001b[0m) \u001b[2;37m│\u001b[0m \u001b[35mtokens:\u001b[0m \u001b[36m0\u001b[0m in • \u001b[32m0\u001b[0m out • \u001b[33m0\u001b[0m total \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mModel:\u001b[0m claude-sonnet-4-5-20250929 \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mStop Reason:\u001b[0m tool_use \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[1;37mContent\u001b[0m (8 blocks) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 1\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_expenses\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01Nq2au3W69RmDFZaSdqe6u1 \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m code execution environment \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"employee_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG007\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"quarter\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"Q3\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 2\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_expenses\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01YYw9cuTSXk7bu7P38qBz6P \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m code execution environment \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"employee_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG005\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"quarter\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"Q3\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 3\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_expenses\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01Fyxxe2KmVpVmw4jJL2CXSz \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m code execution environment \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"employee_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG008\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"quarter\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"Q3\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 4\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_expenses\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01J4ovDu2UJa9Se19vxKTa6y \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m code execution environment \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"employee_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG006\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"quarter\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"Q3\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 5\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_expenses\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01T24CrvQYA3LqGfZftCmueC \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m code execution environment \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"employee_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG003\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"quarter\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"Q3\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 6\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_expenses\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01HotYZN6sk3gMLkpdWXbdz4 \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m code execution environment \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"employee_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG004\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"quarter\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"Q3\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 7\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_expenses\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01AxvEqi3AKqdnH44kGH1U6E \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m code execution environment \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"employee_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG002\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"quarter\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"Q3\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[2;37mBlock 8\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_expenses\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mID:\u001b[0m toolu_01A4agznkK1jA4AyJo3H2jpg \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mCaller:\u001b[0m code execution environment \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"employee_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG001\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"quarter\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"Q3\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m╰─────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "[Container] ID: container_011CVSAwq5J4vNPi3A4P2Rwh\n", + "[Container] Expires at: 2025-11-24 05:41:19.266670+00:00\n", + "[PTC] Tool called from code execution environment: get_expenses\n", + "[PTC] Tool called from code execution environment: get_expenses\n", + "[PTC] Tool called from code execution environment: get_expenses\n", + "[PTC] Tool called from code execution environment: get_expenses\n", + "[PTC] Tool called from code execution environment: get_expenses\n", + "[PTC] Tool called from code execution environment: get_expenses\n", + "[PTC] Tool called from code execution environment: get_expenses\n", + "[PTC] Tool called from code execution environment: get_expenses\n" + ] + }, + { + "data": { + "text/html": [ + "
╭─────────────────────────────────────── Claude API Response ───────────────────────────────────────╮\n",
+       " Claude Message (assistant)  tokens: 4,751 in • 679 out • 5,430 total                             \n",
+       " ├── Model: claude-sonnet-4-5-20250929                                                             \n",
+       " ├── Stop Reason: tool_use                                                                         \n",
+       " └── Content (6 blocks)                                                                            \n",
+       "     ├── Block 1                                                                                   \n",
+       "     │   └── Code Execution Result: Success (exit 0)                                               \n",
+       "     │       └── stdout:                                                                           \n",
+       "     │           └── Found 8 engineering team members                                              \n",
+       "\n",
+       "Employees who exceeded $5,000 standard budget: 3                              \n",
+       "ENG001: Alice Chen - $9177.88                                                 \n",
+       "ENG003: Carol White - $6483.49                                                \n",
+       "ENG007: Grace Taylor - $5289.35                                               \n",
+       "\n",
+       "     ├── Block 2                                                                                   \n",
+       "     │   └── Text                                                                                  \n",
+       "     │       └── Now let me check if any of these three employees have custom budget exceptions:   \n",
+       "     ├── Block 3                                                                                   \n",
+       "     │   └── Server Tool Use                                                                       \n",
+       "     │       ├── ID: srvtoolu_015GTpFmCbd2JPAQLAioB4Qb                                             \n",
+       "     │       ├── Caller: direct                                                                    \n",
+       "     │       └── Code:                                                                             \n",
+       "     │           └──    1                                                                          \n",
+       "   2 import asyncio                                                           \n",
+       "   3 import json                                                              \n",
+       "   4                                                                          \n",
+       "   5 async def main():                                                        \n",
+       "   6     # Check custom budgets for the three employees who exceeded standard \n",
+       "   7     exceeded_ids = ['ENG001', 'ENG003', 'ENG007']                        \n",
+       "   8     exceeded_amounts = {                                                 \n",
+       "   9         'ENG001': {'name': 'Alice Chen', 'travel_total': 9177.88},       \n",
+       "  10         'ENG003': {'name': 'Carol White', 'travel_total': 6483.49},      \n",
+       "  11         'ENG007': {'name': 'Grace Taylor', 'travel_total': 5289.35}      \n",
+       "  12     }                                                                    \n",
+       "  13                                                                          \n",
+       "  14     # Get custom budgets in parallel                                     \n",
+       "  15     budget_tasks = [                                                     \n",
+       "  16         get_custom_budget({'user_id': emp_id})                           \n",
+       "  17         for emp_id in exceeded_ids                                       \n",
+       "  18     ]                                                                    \n",
+       "  19                                                                          \n",
+       "  20     budget_results = await asyncio.gather(*budget_tasks)                 \n",
+       "  21                                                                          \n",
+       "  22     # Analyze who truly exceeded their budget                            \n",
+       "  23     truly_exceeded = []                                                  \n",
+       "  24                                                                          \n",
+       "  25     for i, emp_id in enumerate(exceeded_ids):                            \n",
+       "  26         budget_info = json.loads(budget_results[i])                      \n",
+       "  27         actual_budget = budget_info['travel_budget']                     \n",
+       "  28         travel_total = exceeded_amounts[emp_id]['travel_total']          \n",
+       "  29         name = exceeded_amounts[emp_id]['name']                          \n",
+       "  30                                                                          \n",
+       "  31         if travel_total > actua                                          \n",
+       "  32 ... (truncated)                                                          \n",
+       "     ├── Block 4                                                                                   \n",
+       "     │   └── Tool Use: get_custom_budget                                                           \n",
+       "     │       ├── ID: toolu_01EaaJ3SikeniibPsEdAqXo8                                                \n",
+       "     │       ├── Caller: code execution environment                                                \n",
+       "     │       └── Input:                                                                            \n",
+       "     │           └── {                                                                             \n",
+       "  \"user_id\": \"ENG003\"                                                         \n",
+       "}                                                                             \n",
+       "     ├── Block 5                                                                                   \n",
+       "     │   └── Tool Use: get_custom_budget                                                           \n",
+       "     │       ├── ID: toolu_01E5bqQ4xKX7FTdhh6xLkw4E                                                \n",
+       "     │       ├── Caller: code execution environment                                                \n",
+       "     │       └── Input:                                                                            \n",
+       "     │           └── {                                                                             \n",
+       "  \"user_id\": \"ENG001\"                                                         \n",
+       "}                                                                             \n",
+       "     └── Block 6                                                                                   \n",
+       "         └── Tool Use: get_custom_budget                                                           \n",
+       "             ├── ID: toolu_01PLF38q7ndVQB4mqdqaFt7u                                                \n",
+       "             ├── Caller: code execution environment                                                \n",
+       "             └── Input:                                                                            \n",
+       "                 └── {                                                                             \n",
+       "                       \"user_id\": \"ENG007\"                                                         \n",
+       "                     }                                                                             \n",
+       "╰───────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[36m╭─\u001b[0m\u001b[36m──────────────────────────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[1;36mClaude API Response\u001b[0m\u001b[36m \u001b[0m\u001b[36m──────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[1;36mClaude Message\u001b[0m (\u001b[32massistant\u001b[0m) \u001b[2;37m│\u001b[0m \u001b[35mtokens:\u001b[0m \u001b[36m4,751\u001b[0m in • \u001b[32m679\u001b[0m out • \u001b[33m5,430\u001b[0m total \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mModel:\u001b[0m claude-sonnet-4-5-20250929 \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mStop Reason:\u001b[0m tool_use \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[1;37mContent\u001b[0m (6 blocks) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 1\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mCode Execution Result:\u001b[0m \u001b[32mSuccess (exit 0)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mstdout:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[37mFound 8 engineering team members\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37mEmployees who exceeded $5,000 standard budget: 3\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37mENG001: Alice Chen - $9177.88\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37mENG003: Carol White - $6483.49\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37mENG007: Grace Taylor - $5289.35\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 2\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[36mText\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[37mNow let me check if any of these three employees have custom budget exceptions:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 3\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mServer Tool Use\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m srvtoolu_015GTpFmCbd2JPAQLAioB4Qb \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m direct \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mCode:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 1 \u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 2 \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34mimport\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34masyncio\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 3 \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34mimport\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mjson\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 4 \u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 5 \u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34masync\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34mdef\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;166;226;46;48;2;39;40;34mmain\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 6 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;149;144;119;48;2;39;40;34m# Check custom budgets for the three employees who exceeded standard\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 7 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mexceeded_ids\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mENG001\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mENG003\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mENG007\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 8 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mexceeded_amounts\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m 9 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mENG001\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mname\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mAlice Chen\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mtravel_total\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m9177.88\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m10 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mENG003\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mname\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mCarol White\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mtravel_total\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m6483.49\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m11 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mENG007\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mname\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mGrace Taylor\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mtravel_total\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m5289.35\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m12 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m13 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m14 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;149;144;119;48;2;39;40;34m# Get custom budgets in parallel\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m15 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mbudget_tasks\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m16 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mget_custom_budget\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34muser_id\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34memp_id\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m17 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34mfor\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34memp_id\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34min\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mexceeded_ids\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m18 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m19 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m20 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mbudget_results\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34mawait\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34masyncio\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mgather\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m*\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mbudget_tasks\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m21 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m22 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;149;144;119;48;2;39;40;34m# Analyze who truly exceeded their budget\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m23 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtruly_exceeded\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m24 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m25 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34mfor\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mi\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34memp_id\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34min\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34menumerate\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mexceeded_ids\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m26 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mbudget_info\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mjson\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mloads\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mbudget_results\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mi\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m27 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mactual_budget\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mbudget_info\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mtravel_budget\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m28 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtravel_total\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mexceeded_amounts\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34memp_id\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mtravel_total\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m29 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mname\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mexceeded_amounts\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34memp_id\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mname\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m30 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m31 \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34mif\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtravel_total\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m>\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mactua\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[1;38;2;227;227;221;48;2;39;40;34m \u001b[0m\u001b[38;2;101;102;96;48;2;39;40;34m32 \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtruncated\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 4\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_custom_budget\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01EaaJ3SikeniibPsEdAqXo8 \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m code execution environment \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"user_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG003\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 5\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_custom_budget\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mID:\u001b[0m toolu_01E5bqQ4xKX7FTdhh6xLkw4E \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ ├── \u001b[2;37mCaller:\u001b[0m code execution environment \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"user_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG001\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[2;37mBlock 6\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[33mTool Use:\u001b[0m \u001b[1;33mget_custom_budget\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mID:\u001b[0m toolu_01PLF38q7ndVQB4mqdqaFt7u \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mCaller:\u001b[0m code execution environment \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[32mInput:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m\"user_id\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"ENG007\"\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[38;2;248;248;242;48;2;39;40;34m}\u001b[0m\u001b[48;2;39;40;34m \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m╰───────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "[Container] ID: container_011CVSAwq5J4vNPi3A4P2Rwh\n", + "[Container] Expires at: 2025-11-24 05:41:33.430636+00:00\n", + "[PTC] Tool called from code execution environment: get_custom_budget\n", + "[PTC] Tool called from code execution environment: get_custom_budget\n", + "[PTC] Tool called from code execution environment: get_custom_budget\n" + ] + }, + { + "data": { + "text/html": [ + "
╭────────────────────────────────────────────── Claude API Response ──────────────────────────────────────────────╮\n",
+       " Claude Message (assistant)  tokens: 5,611 in • 205 out • 5,816 total                                           \n",
+       " ├── Model: claude-sonnet-4-5-20250929                                                                           \n",
+       " ├── Stop Reason: end_turn                                                                                       \n",
+       " └── Content (2 blocks)                                                                                          \n",
+       "     ├── Block 1                                                                                                 \n",
+       "     │   └── Code Execution Result: Success (exit 0)                                                             \n",
+       "     │       └── stdout:                                                                                         \n",
+       "     │           └── ENGINEERING TEAM MEMBERS WHO EXCEEDED THEIR Q3 TRAVEL BUDGET:                               \n",
+       "================================================================================            \n",
+       "\n",
+       "Alice Chen (ENG001)                                                                         \n",
+       "  Budget Limit: $5,000.00 (Standard)                                                        \n",
+       "  Travel Spending: $9,177.88                                                                \n",
+       "  Exceeded By: $4,177.88                                                                    \n",
+       "\n",
+       "Carol White (ENG003)                                                                        \n",
+       "  Budget Limit: $5,000.00 (Standard)                                                        \n",
+       "  Travel Spending: $6,483.49                                                                \n",
+       "  Exceeded By: $1,483.49                                                                    \n",
+       "\n",
+       "Grace Taylor (ENG007)                                                                       \n",
+       "  Budget Limit: $5,000.00 (Standard)                                                        \n",
+       "  Travel Spending: $5,289.35                                                                \n",
+       "  Exceeded By: $289.35                                                                      \n",
+       "\n",
+       "     └── Block 2                                                                                                 \n",
+       "         └── Text                                                                                                \n",
+       "             └── ## Summary                                                                                      \n",
+       "                                                                                                                 \n",
+       "                 **Three engineering team members exceeded their Q3 travel budget:**                             \n",
+       "                                                                                                                 \n",
+       "                 1. **Alice Chen (ENG001)**                                                                      \n",
+       "                    - Budget: $5,000 (Standard)                                                                  \n",
+       "                    - Spent: $9,177.88                                                                           \n",
+       "                    - Over budget by: **$4,177.88**                                                              \n",
+       "                                                                                                                 \n",
+       "                 2. **Carol White (ENG003)**                                                                     \n",
+       "                    - Budget: $5,000 (Standard)                                                                  \n",
+       "                    - Spent: $6,483.49                                                                           \n",
+       "                    - Over budget by: **$1,483.49**                                                              \n",
+       "                                                                                                                 \n",
+       "                 3. **Grace Taylor (ENG007)**                                                                    \n",
+       "                    - Budget: $5,000 (Standard)                                                                  \n",
+       "                    - Spent: $5,289.35                                                                           \n",
+       "                    - Over budget by: **$289.35**                                                                \n",
+       "                                                                                                                 \n",
+       "                 All three employees are on the standard $5,000 quarterly travel budget with no custom           \n",
+       "                 exceptions, so they all genuinely exceeded their allocated travel budget for Q3.                \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[36m╭─\u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[1;36mClaude API Response\u001b[0m\u001b[36m \u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[1;36mClaude Message\u001b[0m (\u001b[32massistant\u001b[0m) \u001b[2;37m│\u001b[0m \u001b[35mtokens:\u001b[0m \u001b[36m5,611\u001b[0m in • \u001b[32m205\u001b[0m out • \u001b[33m5,816\u001b[0m total \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mModel:\u001b[0m claude-sonnet-4-5-20250929 \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mStop Reason:\u001b[0m end_turn \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[1;37mContent\u001b[0m (2 blocks) \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m ├── \u001b[2;37mBlock 1\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[33mCode Execution Result:\u001b[0m \u001b[32mSuccess (exit 0)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[32mstdout:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ └── \u001b[37mENGINEERING TEAM MEMBERS WHO EXCEEDED THEIR Q3 TRAVEL BUDGET:\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37m================================================================================\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37mAlice Chen (ENG001)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37m Budget Limit: $5,000.00 (Standard)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37m Travel Spending: $9,177.88\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37m Exceeded By: $4,177.88\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37mCarol White (ENG003)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37m Budget Limit: $5,000.00 (Standard)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37m Travel Spending: $6,483.49\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37m Exceeded By: $1,483.49\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37mGrace Taylor (ENG007)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37m Budget Limit: $5,000.00 (Standard)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37m Travel Spending: $5,289.35\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[37m Exceeded By: $289.35\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m │ \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[2;37mBlock 2\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[36mText\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m └── \u001b[37m## Summary\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m**Three engineering team members exceeded their Q3 travel budget:**\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m1. **Alice Chen (ENG001)**\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m - Budget: $5,000 (Standard)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m - Spent: $9,177.88\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m - Over budget by: **$4,177.88**\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m2. **Carol White (ENG003)**\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m - Budget: $5,000 (Standard)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m - Spent: $6,483.49\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m - Over budget by: **$1,483.49**\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m3. **Grace Taylor (ENG007)**\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m - Budget: $5,000 (Standard)\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m - Spent: $5,289.35\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37m - Over budget by: **$289.35**\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37mAll three employees are on the standard $5,000 quarterly travel budget with no custom \u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[37mexceptions, so they all genuinely exceeded their allocated travel budget for Q3.\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Run the PTC agent\n", + "result_ptc, conversation_ptc, total_tokens_ptc, elapsed_time_ptc, api_count_with_ptc = (\n", + " run_agent_with_ptc(query)\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "da1a0d5d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "============================================================\n", + "Result: ## Summary\n", + "\n", + "**Three engineering team members exceeded their Q3 travel budget:**\n", + "\n", + "1. **Alice Chen (ENG001)**\n", + " - Budget: $5,000 (Standard)\n", + " - Spent: $9,177.88\n", + " - Over budget by: **$4,177.88**\n", + "\n", + "2. **Carol White (ENG003)**\n", + " - Budget: $5,000 (Standard)\n", + " - Spent: $6,483.49\n", + " - Over budget by: **$1,483.49**\n", + "\n", + "3. **Grace Taylor (ENG007)**\n", + " - Budget: $5,000 (Standard)\n", + " - Spent: $5,289.35\n", + " - Over budget by: **$289.35**\n", + "\n", + "All three employees are on the standard $5,000 quarterly travel budget with no custom exceptions, so they all genuinely exceeded their allocated travel budget for Q3.\n", + "\n", + "============================================================\n", + "Performance Metrics:\n", + " Total API calls to Claude: 3\n", + " Total tokens used: 15,919\n", + " Total time taken: 34.88s\n" + ] + } + ], + "source": [ + "print(f\"\\n{'=' * 60}\")\n", + "print(f\"Result: {result_ptc}\")\n", + "print(f\"\\n{'=' * 60}\")\n", + "print(\"Performance Metrics:\")\n", + "print(\n", + " f\" Total API calls to Claude: {len([m for m in conversation_ptc if m['role'] == 'assistant'])}\"\n", + ")\n", + "print(f\" Total tokens used: {total_tokens_ptc:,}\")\n", + "print(f\" Total time taken: {elapsed_time_ptc:.2f}s\")" + ] + }, + { + "cell_type": "markdown", + "id": "xevg4ich93m", + "metadata": {}, + "source": [ + "## Performance Comparison\n", + "\n", + "Let's compare the performance between traditional tool calling and PTC:\n", + "\n", + "**Note on API Call Count:** You may notice that PTC requires more API calls in this example. This is because PTC writes more structured, sequential code that follows best practices—for instance, separating the expense fetching step from the budget checking step. Traditional tool calling can sometimes batch operations together in a single turn, but at the cost of sending all raw data through the model's context. The token efficiency gains from PTC far outweigh the minimal increase in round trips, especially when working with large, metadata-rich datasets." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "fun83cq4bmq", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Metric Traditional PTC\n", + " API Calls 4 4\n", + " Total Tokens 110,473 15,919\n", + "Elapsed Time (s) 35.38 34.88\n", + " Token Reduction - 85.6%\n", + " Time Reduction - 1.4%\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "\n", + "# Create comparison dataframe\n", + "comparison_data = {\n", + " \"Metric\": [\n", + " \"API Calls\",\n", + " \"Total Tokens\",\n", + " \"Elapsed Time (s)\",\n", + " \"Token Reduction\",\n", + " \"Time Reduction\",\n", + " ],\n", + " \"Traditional\": [\n", + " api_count_without_ptc,\n", + " f\"{total_tokens:,}\",\n", + " f\"{elapsed_time:.2f}\",\n", + " \"-\",\n", + " \"-\",\n", + " ],\n", + " \"PTC\": [\n", + " api_count_with_ptc,\n", + " f\"{total_tokens_ptc:,}\",\n", + " f\"{elapsed_time_ptc:.2f}\",\n", + " f\"{((total_tokens - total_tokens_ptc) / total_tokens * 100):.1f}%\",\n", + " f\"{((elapsed_time - elapsed_time_ptc) / elapsed_time * 100):.1f}%\",\n", + " ],\n", + "}\n", + "\n", + "df = pd.DataFrame(comparison_data)\n", + "print(df.to_string(index=False))" + ] + }, + { + "cell_type": "markdown", + "id": "90jm6y08ua7", + "metadata": {}, + "source": [ + "## Key Takeaways\n", + "\n", + "In this example, PTC demonstrated significant performance improvements through three core capabilities:\n", + "\n", + "### 1. Context Preservation Through Large Data Parsing\n", + "This was the primary benefit demonstrated in our workflow. Claude wrote code to fetch and process hundreds of expense line items within the code execution environment. By processing this data programmatically, Claude parsed JSON, filtered by status, summed amounts by category, and compared against budget limits—all without sending the raw expense data and metadata through the model's context window. This resulted in a **significant reduction in token usage**.\n", + "\n", + "### 2. Sequential Dependency Optimization \n", + "The API has a sequential dependency: `get_custom_budget(user_id)` which can only be called after analyzing expenses to identify who exceeded the standard $5,000 budget. In traditional tool calling, this requires multiple round trips—fetch team members, fetch expenses for each person, identify those over budget, then check their custom budgets one by one. With PTC, Claude writes code that orchestrates this entire workflow in the code execution environment, making programmatic tool calls in a loop and maintaining state across calls. This transforms what would be many sequential API round trips into fewer calls with smarter orchestration.\n", + "\n", + "### 3. Computational Logic in Code Execution\n", + "Rather than requiring the model to mentally track and sum dozens of expenses with complex metadata, Claude delegated the arithmetic and aggregation logic to Python code. This reduced cognitive load on the model, ensured precise calculations, and kept irrelevant metadata (like receipt URLs and merchant locations) out of the model's context entirely.\n", + "\n", + "--- \n", + "\n", + "## When to Use PTC\n", + "\n", + "PTC is most beneficial when:\n", + "\n", + "- **Working with large, metadata-rich datasets** that need filtering, parsing, or aggregation (like our expense analysis with receipt URLs, approval chains, merchant details, etc.)\n", + "- **Sequential dependencies exist** where one tool call depends on the results of previous calls (like checking custom budgets only for employees who exceeded standard limits)\n", + "- **Multiple tool calls are needed** in sequence or in loops across similar entities (checking expenses and budgets for each team member)\n", + "- **Computational logic** can reduce what needs to flow through the model's context\n", + "- **Tools are safe** for programmatic/repeated execution without human oversight\n", + "\n", + "## Conclusion\n", + "\n", + "Our team expense analysis demonstrated PTC's strengths: **dramatically reducing context consumption when working with large, metadata-rich datasets** and **optimizing workflows with sequential dependencies**. By allowing Claude to write code that orchestrates tool calls and processes results programmatically, we achieved substantial token savings while maintaining accuracy and insight quality. \n", + "\n", + "PTC is particularly valuable for workflows involving bulk data processing with rich metadata, repeated tool invocations with dependencies, or scenarios where raw tool outputs would otherwise pollute the model's context.\n", + "\n", + "## Next Steps\n", + "\n", + "Try adapting this pattern to your own use cases:\n", + "- Financial data analysis and reporting with sequential lookups\n", + "- Multi-entity health checks that depend on initial scan results \n", + "- Large file processing with metadata (CSV, JSON, XML parsing)\n", + "- Database query result aggregation with follow-up queries\n", + "- Batch API operations with conditional logic based on initial results" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tool_use/requirements.txt b/tool_use/requirements.txt index 76d40390..1dd5b1b5 100644 --- a/tool_use/requirements.txt +++ b/tool_use/requirements.txt @@ -1,3 +1,5 @@ anthropic>=0.18.0 python-dotenv>=1.0.0 -ipykernel>=6.29.0 # For Jupyter in VSCode \ No newline at end of file +ipykernel>=6.29.0 # For Jupyter in VSCode +rich>=13.0.0 +pandas>=2.0.0 \ No newline at end of file diff --git a/tool_use/tool_search_with_embeddings.ipynb b/tool_use/tool_search_with_embeddings.ipynb new file mode 100644 index 00000000..f7444f9c --- /dev/null +++ b/tool_use/tool_search_with_embeddings.ipynb @@ -0,0 +1,1106 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tool Search with Embeddings: Scaling Claude to Thousands of Tools\n", + "\n", + "Building Claude applications with dozens of specialized tools quickly hits a wall: providing all tool definitions upfront consumes your context window, increases latency and costs, and makes it harder for Claude to find the right tool. Beyond ~100 tools, this approach becomes impractical.\n", + "\n", + "Semantic tool search solves this by treating tools as discoverable resources. Instead of front-loading hundreds of definitions, you give Claude a single `tool_search` tool that returns relevant capabilities on demand, cutting context usage by 90%+ while enabling applications that scale to thousands of tools.\n", + "\n", + "**By the end of this cookbook, you'll be able to:**\n", + "- Implement client-side tool search to scale Claude applications from dozens to thousands of tools\n", + "- Use semantic embeddings to dynamically discover relevant tools based on task context\n", + "- Apply this pattern to domain-specific tool libraries (APIs, databases, internal systems)\n", + "\n", + "This pattern is used in production by teams managing large tool ecosystems where context efficiency is critical. While we'll demonstrate with a small set of tools for clarity, the same approach scales seamlessly to libraries with hundreds or thousands of tools.\n", + "\n", + "## Prerequisites\n", + "\n", + "Before following this guide, ensure you have:\n", + "\n", + "**Required Knowledge**\n", + "- Python fundamentals - comfortable with functions, dictionaries, and basic data structures\n", + "- Basic understanding of Claude tool use - we recommend reading the [Tool Use Guide](https://docs.anthropic.com/en/docs/build-with-claude/tool-use) first\n", + "\n", + "**Required Tools**\n", + "- Python 3.11 or higher\n", + "- Anthropic API key ([get one here](https://docs.anthropic.com/claude/reference/getting-started-with-the-api))\n", + "\n", + "## Setup\n", + "\n", + "First, install the required dependencies:" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks...\n", + "To disable this warning, you can either:\n", + "\t- Avoid using `tokenizers` before the fork if possible\n", + "\t- Explicitly set the environment variable TOKENIZERS_PARALLELISM=(true | false)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# Note: we use -q to avoid printing too much to stdout\n", + "# Use --only-binary to avoid build issues with pythran\n", + "%pip install --only-binary :all: -q anthropic sentence-transformers numpy python-dotenv" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ensure your `.env` file contains:\n", + "```\n", + "ANTHROPIC_API_KEY=your_key_here\n", + "```\n", + "\n", + "Load your environment variables and configure the client:" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading SentenceTransformer model...\n", + "✓ Clients initialized successfully\n" + ] + } + ], + "source": [ + "import anthropic\n", + "from sentence_transformers import SentenceTransformer\n", + "import numpy as np\n", + "import json\n", + "from typing import List, Dict, Any\n", + "from dotenv import load_dotenv\n", + "import random\n", + "from datetime import datetime, timedelta\n", + "\n", + "# Load environment variables from .env file\n", + "load_dotenv()\n", + "\n", + "# Define model constant for easy updates\n", + "MODEL = \"claude-sonnet-4-5-20250929\"\n", + "\n", + "# Initialize Claude client (API key loaded from environment)\n", + "claude_client = anthropic.Anthropic()\n", + "\n", + "# Load the SentenceTransformer model\n", + "# all-MiniLM-L6-v2 is a lightweight model with 384 dimensional embeddings\n", + "# It will be downloaded from HuggingFace on first use\n", + "print(\"Loading SentenceTransformer model...\")\n", + "embedding_model = SentenceTransformer(\"sentence-transformers/all-MiniLM-L6-v2\")\n", + "\n", + "print(\"✓ Clients initialized successfully\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define Tool Library\n", + "\n", + "Before we can implement semantic search, we need tools to search through. We'll create a library of 8 tools across two categories: Weather and Finance.\n", + "\n", + "In production applications, you might manage hundreds or thousands of tools across your internal APIs, database operations, or third-party integrations. The semantic search approach scales to these larger libraries without modification - we're using a small set here purely for demonstration clarity." + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Defined 8 tools in the library\n" + ] + } + ], + "source": [ + "# Define our tool library with 2 domains\n", + "TOOL_LIBRARY = [\n", + " # Weather Tools\n", + " {\n", + " \"name\": \"get_weather\",\n", + " \"description\": \"Get the current weather in a given location\",\n", + " \"input_schema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"location\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The city and state, e.g. San Francisco, CA\",\n", + " },\n", + " \"unit\": {\n", + " \"type\": \"string\",\n", + " \"enum\": [\"celsius\", \"fahrenheit\"],\n", + " \"description\": \"The unit of temperature\",\n", + " },\n", + " },\n", + " \"required\": [\"location\"],\n", + " },\n", + " },\n", + " {\n", + " \"name\": \"get_forecast\",\n", + " \"description\": \"Get the weather forecast for multiple days ahead\",\n", + " \"input_schema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"location\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The city and state\",\n", + " },\n", + " \"days\": {\n", + " \"type\": \"number\",\n", + " \"description\": \"Number of days to forecast (1-10)\",\n", + " },\n", + " },\n", + " \"required\": [\"location\", \"days\"],\n", + " },\n", + " },\n", + " {\n", + " \"name\": \"get_timezone\",\n", + " \"description\": \"Get the current timezone and time for a location\",\n", + " \"input_schema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"location\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"City name or timezone identifier\",\n", + " }\n", + " },\n", + " \"required\": [\"location\"],\n", + " },\n", + " },\n", + " {\n", + " \"name\": \"get_air_quality\",\n", + " \"description\": \"Get current air quality index and pollutant levels for a location\",\n", + " \"input_schema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"location\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"City name or coordinates\",\n", + " }\n", + " },\n", + " \"required\": [\"location\"],\n", + " },\n", + " },\n", + " # Finance Tools\n", + " {\n", + " \"name\": \"get_stock_price\",\n", + " \"description\": \"Get the current stock price and market data for a given ticker symbol\",\n", + " \"input_schema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"ticker\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Stock ticker symbol (e.g., AAPL, GOOGL)\",\n", + " },\n", + " \"include_history\": {\n", + " \"type\": \"boolean\",\n", + " \"description\": \"Include historical data\",\n", + " },\n", + " },\n", + " \"required\": [\"ticker\"],\n", + " },\n", + " },\n", + " {\n", + " \"name\": \"convert_currency\",\n", + " \"description\": \"Convert an amount from one currency to another using current exchange rates\",\n", + " \"input_schema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"amount\": {\n", + " \"type\": \"number\",\n", + " \"description\": \"Amount to convert\",\n", + " },\n", + " \"from_currency\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Source currency code (e.g., USD)\",\n", + " },\n", + " \"to_currency\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Target currency code (e.g., EUR)\",\n", + " },\n", + " },\n", + " \"required\": [\"amount\", \"from_currency\", \"to_currency\"],\n", + " },\n", + " },\n", + " {\n", + " \"name\": \"calculate_compound_interest\",\n", + " \"description\": \"Calculate compound interest for investments over time\",\n", + " \"input_schema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"principal\": {\n", + " \"type\": \"number\",\n", + " \"description\": \"Initial investment amount\",\n", + " },\n", + " \"rate\": {\n", + " \"type\": \"number\",\n", + " \"description\": \"Annual interest rate (as percentage)\",\n", + " },\n", + " \"years\": {\"type\": \"number\", \"description\": \"Number of years\"},\n", + " \"frequency\": {\n", + " \"type\": \"string\",\n", + " \"enum\": [\"daily\", \"monthly\", \"quarterly\", \"annually\"],\n", + " \"description\": \"Compounding frequency\",\n", + " },\n", + " },\n", + " \"required\": [\"principal\", \"rate\", \"years\"],\n", + " },\n", + " },\n", + " {\n", + " \"name\": \"get_market_news\",\n", + " \"description\": \"Get recent financial news and market updates for a specific company or sector\",\n", + " \"input_schema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"query\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Company name, ticker symbol, or sector\",\n", + " },\n", + " \"limit\": {\n", + " \"type\": \"number\",\n", + " \"description\": \"Maximum number of news articles to return\",\n", + " },\n", + " },\n", + " \"required\": [\"query\"],\n", + " },\n", + " },\n", + "]\n", + "\n", + "print(f\"✓ Defined {len(TOOL_LIBRARY)} tools in the library\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create Tool Embeddings\n", + "\n", + "Semantic search works by comparing the *meaning* of text, rather than just searching for keywords. To enable this, we need to convert each tool definition into an **embedding vector** that captures its semantic meaning.\n", + "\n", + "Since our tool definitions are structured JSON objects with names, descriptions, and parameters, we first convert each tool into a human-readable text representation, then generate embedding vectors using SentenceTransformer's `all-MiniLM-L6-v2` model.\n", + "\n", + "We picked this model because it is:\n", + "- **Lightweight and fast** (only 384 dimensions vs 768+ for larger models)\n", + "- **Runs locally** without requiring API calls\n", + "- **Sufficient for tool search** (you can experiment with larger models for better accuracy)\n", + "\n", + "Let's start by creating a function that converts tool definitions into searchable text:" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample tool text representation:\n", + "Tool: get_weather\n", + "Description: Get the current weather in a given location\n", + "Parameters: location (string): The city and state, e.g. San Francisco, CA, unit (string): The unit of temperature\n" + ] + } + ], + "source": [ + "def tool_to_text(tool: Dict[str, Any]) -> str:\n", + " \"\"\"\n", + " Convert a tool definition into a text representation for embedding.\n", + " Combines the tool name, description, and parameter information.\n", + " \"\"\"\n", + " text_parts = [\n", + " f\"Tool: {tool['name']}\",\n", + " f\"Description: {tool['description']}\",\n", + " ]\n", + "\n", + " # Add parameter information\n", + " if \"input_schema\" in tool and \"properties\" in tool[\"input_schema\"]:\n", + " params = tool[\"input_schema\"][\"properties\"]\n", + " param_descriptions = []\n", + " for param_name, param_info in params.items():\n", + " param_desc = param_info.get(\"description\", \"\")\n", + " param_type = param_info.get(\"type\", \"\")\n", + " param_descriptions.append(\n", + " f\"{param_name} ({param_type}): {param_desc}\"\n", + " )\n", + "\n", + " if param_descriptions:\n", + " text_parts.append(\"Parameters: \" + \", \".join(param_descriptions))\n", + "\n", + " return \"\\n\".join(text_parts)\n", + "\n", + "\n", + "# Test with one tool\n", + "sample_text = tool_to_text(TOOL_LIBRARY[0])\n", + "print(\"Sample tool text representation:\")\n", + "print(sample_text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's create embeddings for all our tools:" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Creating embeddings for all tools...\n", + "✓ Created embeddings with shape: (8, 384)\n", + " - 8 tools\n", + " - 384 dimensions per embedding\n" + ] + } + ], + "source": [ + "# Create embeddings for all tools\n", + "print(\"Creating embeddings for all tools...\")\n", + "\n", + "tool_texts = [tool_to_text(tool) for tool in TOOL_LIBRARY]\n", + "\n", + "# Embed all tools at once using SentenceTransformer\n", + "# The model returns normalized embeddings by default\n", + "tool_embeddings = embedding_model.encode(tool_texts, convert_to_numpy=True)\n", + "\n", + "print(f\"✓ Created embeddings with shape: {tool_embeddings.shape}\")\n", + "print(f\" - {tool_embeddings.shape[0]} tools\")\n", + "print(f\" - {tool_embeddings.shape[1]} dimensions per embedding\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Implement Tool Search\n", + "\n", + "With our tools embedded as vectors, we can now implement semantic search. If two pieces of text have similar meanings, their embedding vectors will be close together in vector space. We measure this \"closeness\" using **cosine similarity**.\n", + "\n", + "The search process:\n", + "1. **Embed the query**: Convert Claude's natural language search request into the same vector space as our tools\n", + "2. **Calculate similarity**: Compute cosine similarity between the query vector and each tool vector\n", + "3. **Rank and return**: Sort tools by similarity score and return the top N matches\n", + "\n", + "With semantic search, Claude can search using natural language like \"I need to check the weather\" or \"calculate investment returns\" rather than exact tool names.\n", + "\n", + "Let's implement the search function and test it with a sample query:" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Search query: 'I need to check the weather'\n", + "\n", + "Top 3 matching tools:\n", + "1. get_weather (similarity: 0.560)\n", + "2. get_forecast (similarity: 0.508)\n", + "3. get_air_quality (similarity: 0.401)\n" + ] + } + ], + "source": [ + "def search_tools(query: str, top_k: int = 5) -> List[Dict[str, Any]]:\n", + " \"\"\"\n", + " Search for tools using semantic similarity.\n", + "\n", + " Args:\n", + " query: Natural language description of what tool is needed\n", + " top_k: Number of top tools to return\n", + "\n", + " Returns:\n", + " List of tool definitions most relevant to the query\n", + " \"\"\"\n", + " # Embed the query using SentenceTransformer\n", + " query_embedding = embedding_model.encode(query, convert_to_numpy=True)\n", + "\n", + " # Calculate cosine similarity using dot product\n", + " # SentenceTransformer returns normalized embeddings, so dot product = cosine similarity\n", + " similarities = np.dot(tool_embeddings, query_embedding)\n", + "\n", + " # Get top k indices\n", + " top_indices = np.argsort(similarities)[-top_k:][::-1]\n", + "\n", + " # Return the corresponding tools with their scores\n", + " results = []\n", + " for idx in top_indices:\n", + " results.append(\n", + " {\"tool\": TOOL_LIBRARY[idx], \"similarity_score\": float(similarities[idx])}\n", + " )\n", + "\n", + " return results\n", + "\n", + "\n", + "# Test the search function\n", + "test_query = \"I need to check the weather\"\n", + "test_results = search_tools(test_query, top_k=3)\n", + "\n", + "print(f\"Search query: '{test_query}'\\n\")\n", + "print(\"Top 3 matching tools:\")\n", + "for i, result in enumerate(test_results, 1):\n", + " tool_name = result[\"tool\"][\"name\"]\n", + " score = result[\"similarity_score\"]\n", + " print(f\"{i}. {tool_name} (similarity: {score:.3f})\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define the tool_search Tool\n", + "\n", + "Now we'll implement the **meta-tool** that allows Claude to discover other tools on demand. When Claude needs a capability it doesn't have, it searches for it using this `tool_search` tool, receives the tool definitions in the result, and can use those newly discovered tools immediately.\n", + "\n", + "This is the only tool we provide to Claude initially:" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Tool search definition created\n" + ] + } + ], + "source": [ + "# The tool_search tool definition\n", + "TOOL_SEARCH_DEFINITION = {\n", + " \"name\": \"tool_search\",\n", + " \"description\": \"Search for available tools that can help with a task. Returns tool definitions for matching tools. Use this when you need a tool but don't have it available yet.\",\n", + " \"input_schema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"query\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Natural language description of what kind of tool you need (e.g., 'weather information', 'currency conversion', 'stock prices')\",\n", + " },\n", + " \"top_k\": {\n", + " \"type\": \"number\",\n", + " \"description\": \"Number of tools to return (default: 5)\",\n", + " },\n", + " },\n", + " \"required\": [\"query\"],\n", + " },\n", + "}\n", + "\n", + "print(\"✓ Tool search definition created\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's implement the handler that processes `tool_search` calls from Claude and returns discovered tools:" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "🔍 Tool search: 'stock market data'\n", + " Found 3 tools:\n", + " 1. get_stock_price (similarity: 0.524)\n", + " 2. get_market_news (similarity: 0.469)\n", + " 3. calculate_compound_interest (similarity: 0.244)\n", + "\n", + "Returned 3 tool references:\n", + " {'type': 'tool_reference', 'tool_name': 'get_stock_price'}\n", + " {'type': 'tool_reference', 'tool_name': 'get_market_news'}\n", + " {'type': 'tool_reference', 'tool_name': 'calculate_compound_interest'}\n" + ] + } + ], + "source": [ + "def handle_tool_search(query: str, top_k: int = 5) -> List[Dict[str, Any]]:\n", + " \"\"\"\n", + " Handle a tool_search invocation and return tool references.\n", + "\n", + " Returns a list of tool_reference content blocks for discovered tools.\n", + " \"\"\"\n", + " # Search for relevant tools\n", + " results = search_tools(query, top_k=top_k)\n", + "\n", + " # Create tool_reference objects instead of full definitions\n", + " tool_references = [\n", + " {\"type\": \"tool_reference\", \"tool_name\": result[\"tool\"][\"name\"]}\n", + " for result in results\n", + " ]\n", + "\n", + " print(f\"\\n🔍 Tool search: '{query}'\")\n", + " print(f\" Found {len(tool_references)} tools:\")\n", + " for i, result in enumerate(results, 1):\n", + " print(\n", + " f\" {i}. {result['tool']['name']} (similarity: {result['similarity_score']:.3f})\"\n", + " )\n", + "\n", + " return tool_references\n", + "\n", + "\n", + "# Test the handler\n", + "test_result = handle_tool_search(\"stock market data\", top_k=3)\n", + "print(f\"\\nReturned {len(test_result)} tool references:\")\n", + "for ref in test_result:\n", + " print(f\" {ref}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Mock Tool Execution\n", + "\n", + "For this demonstration, we'll create mock responses for tool executions. In a real application, these would call actual APIs or services:" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Mock tool execution function created\n" + ] + } + ], + "source": [ + "def mock_tool_execution(tool_name: str, tool_input: Dict[str, Any]) -> str:\n", + " \"\"\"\n", + " Generate realistic mock responses for tool executions.\n", + "\n", + " Args:\n", + " tool_name: Name of the tool being executed\n", + " tool_input: Input parameters for the tool\n", + "\n", + " Returns:\n", + " Mock response string appropriate for the tool\n", + " \"\"\"\n", + " # Weather tools\n", + " if tool_name == \"get_weather\":\n", + " location = tool_input.get(\"location\", \"Unknown\")\n", + " unit = tool_input.get(\"unit\", \"fahrenheit\")\n", + " temp = (\n", + " random.randint(15, 30)\n", + " if unit == \"celsius\"\n", + " else random.randint(60, 85)\n", + " )\n", + " conditions = random.choice([\"sunny\", \"partly cloudy\", \"cloudy\", \"rainy\"])\n", + " return json.dumps(\n", + " {\n", + " \"location\": location,\n", + " \"temperature\": temp,\n", + " \"unit\": unit,\n", + " \"conditions\": conditions,\n", + " \"humidity\": random.randint(40, 80),\n", + " \"wind_speed\": random.randint(5, 20),\n", + " }\n", + " )\n", + "\n", + " elif tool_name == \"get_forecast\":\n", + " location = tool_input.get(\"location\", \"Unknown\")\n", + " days = int(tool_input.get(\"days\", 5))\n", + " forecast = []\n", + " for i in range(days):\n", + " date = (datetime.now() + timedelta(days=i)).strftime(\"%Y-%m-%d\")\n", + " forecast.append(\n", + " {\n", + " \"date\": date,\n", + " \"high\": random.randint(20, 30),\n", + " \"low\": random.randint(10, 20),\n", + " \"conditions\": random.choice(\n", + " [\"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\"]\n", + " ),\n", + " }\n", + " )\n", + " return json.dumps({\"location\": location, \"forecast\": forecast})\n", + "\n", + " elif tool_name == \"get_timezone\":\n", + " location = tool_input.get(\"location\", \"Unknown\")\n", + " return json.dumps(\n", + " {\n", + " \"location\": location,\n", + " \"timezone\": \"UTC+9\",\n", + " \"current_time\": datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"),\n", + " \"utc_offset\": \"+09:00\",\n", + " }\n", + " )\n", + "\n", + " elif tool_name == \"get_air_quality\":\n", + " location = tool_input.get(\"location\", \"Unknown\")\n", + " aqi = random.randint(20, 150)\n", + " categories = {\n", + " (0, 50): \"Good\",\n", + " (51, 100): \"Moderate\",\n", + " (101, 150): \"Unhealthy for Sensitive Groups\",\n", + " }\n", + " category = next(\n", + " cat for (low, high), cat in categories.items() if low <= aqi <= high\n", + " )\n", + " return json.dumps(\n", + " {\n", + " \"location\": location,\n", + " \"aqi\": aqi,\n", + " \"category\": category,\n", + " \"pollutants\": {\n", + " \"pm25\": random.randint(5, 50),\n", + " \"pm10\": random.randint(10, 100),\n", + " \"o3\": random.randint(20, 80),\n", + " },\n", + " }\n", + " )\n", + "\n", + " # Finance tools\n", + " elif tool_name == \"get_stock_price\":\n", + " ticker = tool_input.get(\"ticker\", \"UNKNOWN\")\n", + " return json.dumps(\n", + " {\n", + " \"ticker\": ticker,\n", + " \"price\": round(random.uniform(100, 500), 2),\n", + " \"change\": round(random.uniform(-5, 5), 2),\n", + " \"change_percent\": round(random.uniform(-2, 2), 2),\n", + " \"volume\": random.randint(1000000, 10000000),\n", + " \"market_cap\": f\"${random.randint(100, 1000)}B\",\n", + " }\n", + " )\n", + "\n", + " elif tool_name == \"convert_currency\":\n", + " amount = tool_input.get(\"amount\", 0)\n", + " from_currency = tool_input.get(\"from_currency\", \"USD\")\n", + " to_currency = tool_input.get(\"to_currency\", \"EUR\")\n", + " # Mock exchange rate\n", + " rate = random.uniform(0.8, 1.2)\n", + " converted = round(amount * rate, 2)\n", + " return json.dumps(\n", + " {\n", + " \"original_amount\": amount,\n", + " \"from_currency\": from_currency,\n", + " \"to_currency\": to_currency,\n", + " \"exchange_rate\": round(rate, 4),\n", + " \"converted_amount\": converted,\n", + " }\n", + " )\n", + "\n", + " elif tool_name == \"calculate_compound_interest\":\n", + " principal = tool_input.get(\"principal\", 0)\n", + " rate = tool_input.get(\"rate\", 0)\n", + " years = tool_input.get(\"years\", 0)\n", + " frequency = tool_input.get(\"frequency\", \"monthly\")\n", + "\n", + " # Calculate compound interest\n", + " n_map = {\"daily\": 365, \"monthly\": 12, \"quarterly\": 4, \"annually\": 1}\n", + " n = n_map.get(frequency, 12)\n", + " final_amount = principal * (1 + rate / 100 / n) ** (n * years)\n", + " interest_earned = final_amount - principal\n", + "\n", + " return json.dumps(\n", + " {\n", + " \"principal\": principal,\n", + " \"rate\": rate,\n", + " \"years\": years,\n", + " \"compounding_frequency\": frequency,\n", + " \"final_amount\": round(final_amount, 2),\n", + " \"interest_earned\": round(interest_earned, 2),\n", + " }\n", + " )\n", + "\n", + " elif tool_name == \"get_market_news\":\n", + " query = tool_input.get(\"query\", \"\")\n", + " limit = tool_input.get(\"limit\", 5)\n", + " news = []\n", + " for i in range(min(limit, 5)):\n", + " news.append(\n", + " {\n", + " \"title\": f\"{query} - News Article {i+1}\",\n", + " \"source\": random.choice(\n", + " [\n", + " \"Bloomberg\",\n", + " \"Reuters\",\n", + " \"Financial Times\",\n", + " \"Wall Street Journal\",\n", + " ]\n", + " ),\n", + " \"published\": (datetime.now() - timedelta(hours=random.randint(1, 24))).strftime(\n", + " \"%Y-%m-%d %H:%M\"\n", + " ),\n", + " \"summary\": f\"Latest developments regarding {query}...\",\n", + " }\n", + " )\n", + " return json.dumps({\"query\": query, \"articles\": news, \"count\": len(news)})\n", + "\n", + " # Default fallback\n", + " else:\n", + " return json.dumps(\n", + " {\n", + " \"status\": \"executed\",\n", + " \"tool\": tool_name,\n", + " \"message\": f\"Tool {tool_name} executed successfully with input: {json.dumps(tool_input)}\",\n", + " }\n", + " )\n", + "\n", + "\n", + "print(\"✓ Mock tool execution function created\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Implement Conversation Loop\n", + "\n", + "Now let's put it all together! We'll create a conversation loop that handles the complete tool search workflow.\n", + "\n", + "**The conversation flow:**\n", + "1. Claude starts with only the `tool_search` tool available\n", + "2. When Claude calls `tool_search`, we run semantic search and return matching tool definitions\n", + "3. Claude can then use the discovered tools immediately\n", + "4. When Claude calls a discovered tool, we execute it (using mock responses for this demo)\n", + "5. The loop continues until Claude has a final answer" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Conversation loop implemented\n" + ] + } + ], + "source": [ + "def run_tool_search_conversation(user_message: str, max_turns: int = 5) -> None:\n", + " \"\"\"\n", + " Run a conversation with Claude using the tool search pattern.\n", + "\n", + " Args:\n", + " user_message: The initial user message\n", + " max_turns: Maximum number of conversation turns\n", + " \"\"\"\n", + " print(f\"\\n{'='*80}\")\n", + " print(f\"USER: {user_message}\")\n", + " print(f\"{'='*80}\\n\")\n", + "\n", + " # Initialize conversation with only tool_search available\n", + " messages = [{\"role\": \"user\", \"content\": user_message}]\n", + "\n", + " for turn in range(max_turns):\n", + " print(f\"\\n--- Turn {turn + 1} ---\")\n", + "\n", + " # Call Claude with current message history\n", + " response = claude_client.messages.create(\n", + " model=MODEL,\n", + " max_tokens=1024,\n", + " tools=TOOL_LIBRARY + [TOOL_SEARCH_DEFINITION],\n", + " messages=messages,\n", + " # IMPORTANT: This beta header enables tool definitions in tool results\n", + " extra_headers={\n", + " \"anthropic-beta\": \"advanced-tool-use-2025-11-20\"\n", + " },\n", + " )\n", + "\n", + " # Add assistant's response to messages\n", + " messages.append({\"role\": \"assistant\", \"content\": response.content})\n", + "\n", + " # Check if we're done\n", + " if response.stop_reason == \"end_turn\":\n", + " print(\"\\n✓ Conversation complete\\n\")\n", + " # Print final response\n", + " for block in response.content:\n", + " if block.type == \"text\":\n", + " print(f\"ASSISTANT: {block.text}\")\n", + " break\n", + "\n", + " # Handle tool uses\n", + " if response.stop_reason == \"tool_use\":\n", + " tool_results = []\n", + "\n", + " for block in response.content:\n", + " if block.type == \"text\":\n", + " print(f\"\\nASSISTANT: {block.text}\")\n", + "\n", + " elif block.type == \"tool_use\":\n", + " tool_name = block.name\n", + " tool_input = block.input\n", + " tool_use_id = block.id\n", + "\n", + " print(f\"\\n🔧 Tool invocation: {tool_name}\")\n", + " print(f\" Input: {json.dumps(tool_input, indent=2)}\")\n", + "\n", + " if tool_name == \"tool_search\":\n", + " # Handle tool search\n", + " query = tool_input[\"query\"]\n", + " top_k = tool_input.get(\"top_k\", 5)\n", + "\n", + " # Get tool references\n", + " tool_references = handle_tool_search(query, top_k)\n", + "\n", + " # Create tool result with tool_reference content blocks\n", + " tool_results.append(\n", + " {\n", + " \"type\": \"tool_result\",\n", + " \"tool_use_id\": tool_use_id,\n", + " \"content\": tool_references,\n", + " }\n", + " )\n", + " else:\n", + " # Execute the discovered tool with mock data\n", + " mock_result = mock_tool_execution(tool_name, tool_input)\n", + "\n", + " # Print a preview of the result\n", + " if len(mock_result) > 150:\n", + " print(f\" ✅ Mock result: {mock_result[:150]}...\")\n", + " else:\n", + " print(f\" ✅ Mock result: {mock_result}\")\n", + "\n", + " tool_results.append(\n", + " {\n", + " \"type\": \"tool_result\",\n", + " \"tool_use_id\": tool_use_id,\n", + " \"content\": mock_result,\n", + " }\n", + " )\n", + "\n", + " # Add tool results to messages\n", + " if tool_results:\n", + " messages.append({\"role\": \"user\", \"content\": tool_results})\n", + " else:\n", + " print(f\"\\nUnexpected stop reason: {response.stop_reason}\")\n", + " break\n", + "\n", + " print(f\"\\n{'='*80}\\n\")\n", + "\n", + "\n", + "print(\"✓ Conversation loop implemented\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 1: Weather Query\n", + "\n", + "Let's test with a simple weather question. Claude should:\n", + "1. Call `tool_search` to find weather tools\n", + "2. Receive weather tool definitions in the result\n", + "3. Use one of the discovered tools" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "================================================================================\n", + "USER: What's the weather like in Tokyo?\n", + "================================================================================\n", + "\n", + "\n", + "--- Turn 1 ---\n", + "\n", + "🔧 Tool invocation: get_weather\n", + " Input: {\n", + " \"location\": \"Tokyo\"\n", + "}\n", + " ✅ Mock result: {\"location\": \"Tokyo\", \"temperature\": 75, \"unit\": \"fahrenheit\", \"conditions\": \"partly cloudy\", \"humidity\": 61, \"wind_speed\": 9}\n", + "\n", + "--- Turn 2 ---\n", + "\n", + "✓ Conversation complete\n", + "\n", + "ASSISTANT: The weather in Tokyo is currently:\n", + "- **Temperature:** 75°F (about 24°C)\n", + "- **Conditions:** Partly cloudy\n", + "- **Humidity:** 61%\n", + "- **Wind Speed:** 9 mph\n", + "\n", + "It's a pleasant day with comfortable temperatures and some cloud cover!\n", + "\n", + "================================================================================\n", + "\n" + ] + } + ], + "source": [ + "run_tool_search_conversation(\"What's the weather like in Tokyo?\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 2: Finance Query\n", + "\n", + "Let's try a financial calculation query that requires discovering and using finance tools:" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "================================================================================\n", + "USER: If I invest $10,000 at 5% annual interest for 10 years with monthly compounding, how much will I have?\n", + "================================================================================\n", + "\n", + "\n", + "--- Turn 1 ---\n", + "\n", + "🔧 Tool invocation: calculate_compound_interest\n", + " Input: {\n", + " \"principal\": 10000,\n", + " \"rate\": 5,\n", + " \"years\": 10,\n", + " \"frequency\": \"monthly\"\n", + "}\n", + " ✅ Mock result: {\"principal\": 10000, \"rate\": 5, \"years\": 10, \"compounding_frequency\": \"monthly\", \"final_amount\": 16470.09, \"interest_earned\": 6470.09}\n", + "\n", + "--- Turn 2 ---\n", + "\n", + "✓ Conversation complete\n", + "\n", + "ASSISTANT: If you invest $10,000 at 5% annual interest for 10 years with monthly compounding, you will have:\n", + "\n", + "**Final Amount: $16,470.09**\n", + "\n", + "This means you'll earn **$6,470.09** in interest over the 10-year period.\n", + "\n", + "The monthly compounding means that interest is calculated and added to your principal every month, which allows your investment to grow faster than with annual compounding due to the effect of earning \"interest on interest\" more frequently.\n", + "\n", + "================================================================================\n", + "\n" + ] + } + ], + "source": [ + "run_tool_search_conversation(\n", + " \"If I invest $10,000 at 5% annual interest for 10 years with monthly compounding, how much will I have?\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "In this cookbook, we implemented a client-side tool search system that enables Claude to work with large tool libraries efficiently. We covered:\n", + "\n", + "- **Semantic tool discovery**: Using embeddings to match natural language queries to relevant tools, enabling Claude to find the right capability without seeing all available tools upfront\n", + "- **Dynamic tool loading**: Returning tool definitions in tool results using Claude's tool search feature, allowing Claude to discover and immediately use new tools mid-conversation\n", + "- **Context optimization**: Reducing initial context from thousands of tokens (19+ tool definitions) to just the single `tool_search` definition, cutting context usage by 90%+\n", + "\n", + "### Applying This to Your Projects\n", + "\n", + "Consider tool search when:\n", + "- You have **>20 specialized tools** and context usage becomes a concern\n", + "- Your tool library **grows over time** and manual curation becomes impractical\n", + "- You need to support **domain-specific APIs** with hundreds of endpoints (database operations, internal microservices, third-party integrations)\n", + "- **Cost and latency optimization** are priorities for your application\n", + "\n", + "### Next Steps\n", + "\n", + "To take this implementation further:\n", + "\n", + "1. **Persist embeddings**: Cache embeddings to disk to avoid recomputing on every session, reducing startup time\n", + "2. **Improve search quality**: Experiment with different embedding models (e.g., larger models like `all-mpnet-base-v2`) or implement hybrid search combining semantic and keyword matching (BM25)\n", + "3. **Scale to larger libraries**: Test with hundreds or thousands of tools to see how the pattern performs at production scale\n", + "4. **Add tool metadata**: Include usage statistics, cost information, or reliability scores in your search ranking\n", + "5. **Implement caching**: Cache frequently used tool definitions to reduce repeated searches\n", + "\n", + "### Further Reading\n", + "\n", + "- [Claude Tool Use Guide](https://docs.anthropic.com/en/docs/build-with-claude/tool-use) - Comprehensive guide to building with tools\n", + "- [SentenceTransformers Documentation](https://www.sbert.net/) - Learn more about embedding models and semantic search\n", + "- [Tool Search Tool Documentation](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-search) - Official documentation on the tool search pattern" + ] + } + ], + "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.14" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/tool_use/utils/__init__.py b/tool_use/utils/__init__.py new file mode 100644 index 00000000..9c9f38ff --- /dev/null +++ b/tool_use/utils/__init__.py @@ -0,0 +1,11 @@ +""" +Shared utilities for Claude tool use cookbooks. + +This package contains reusable components for creating cookbook demonstrations: +- visualize: Rich terminal visualization for Claude API responses +- team_expense_api: Example mock API for team expense management demonstrations +""" + +from .visualize import show_response, visualize + +__all__ = ["visualize", "show_response"] diff --git a/tool_use/utils/customer_service_api.py b/tool_use/utils/customer_service_api.py new file mode 100644 index 00000000..2bec8549 --- /dev/null +++ b/tool_use/utils/customer_service_api.py @@ -0,0 +1,363 @@ +""" +Customer Support Ticket System - Mock API +Generate realistic support tickets for compaction demonstration +""" + +import random +from dataclasses import dataclass, field +from datetime import datetime, timedelta +from enum import Enum + + +# Ticket Categories +class TicketCategory(str, Enum): + BILLING = "billing" + TECHNICAL = "technical" + ACCOUNT = "account" + PRODUCT = "product" + SHIPPING = "shipping" + + +class TicketPriority(str, Enum): + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + URGENT = "urgent" + + +class TicketStatus(str, Enum): + NEW = "new" + OPEN = "open" + PENDING = "pending" + RESOLVED = "resolved" + CLOSED = "closed" + + +@dataclass +class Ticket: + """Represents a customer support ticket""" + + id: str + customer_name: str + customer_email: str + subject: str + description: str + category: TicketCategory | None = None + priority: TicketPriority | None = None + status: TicketStatus = TicketStatus.NEW + created_at: datetime | None = None + assigned_team: str | None = None + notes: list[str] = field(default_factory=list) + + def __post_init__(self): + if self.created_at is None: + self.created_at = datetime.now() + + +# Mock data generation +class TicketGenerator: + """Generate realistic support tickets for testing""" + + TICKET_TEMPLATES = [ + # Billing issues + { + "category": TicketCategory.BILLING, + "subjects": [ + "Double charged for subscription", + "Unable to update payment method", + "Unexpected charge on my account", + "Refund request for cancelled service", + "Billing cycle confusion", + ], + "descriptions": [ + "I was charged twice for my monthly subscription. The first charge was on {date1} for ${amount} and another on {date2} for ${amount}. Please refund the duplicate charge.", + "I've been trying to update my credit card information for the past week but keep getting an error message saying 'Payment method could not be updated.' My card expires next month.", + "I noticed a charge of ${amount} on {date1} that I don't recognize. I haven't used the service in over a month. Can you help me understand what this charge is for?", + "I cancelled my subscription on {date1} but was still charged on {date2}. According to your terms, I should not have been billed. Please process a refund.", + "I'm confused about when my billing cycle starts. I signed up on {date1} but was charged on {date2}, which seems early. Can you clarify?", + ], + }, + # Technical issues + { + "category": TicketCategory.TECHNICAL, + "subjects": [ + "Application crashes on startup", + "Cannot sync data across devices", + "Export feature not working", + "Slow performance after recent update", + "Error message when uploading files", + ], + "descriptions": [ + "Ever since the latest update ({version}), the app crashes immediately when I try to open it. I'm on {device} running {os}. I've tried reinstalling but the problem persists.", + "My data isn't syncing between my phone and desktop. I made changes on my phone yesterday but they're not showing up on my computer. Both devices are connected to the internet.", + "When I try to export my data as CSV, I get an error: '{error_msg}'. This has been happening for the past 3 days. The export to PDF works fine.", + "Since updating to version {version} last week, the app has become noticeably slower. Loading times have increased from 2-3 seconds to 15-20 seconds.", + "I'm trying to upload a {file_type} file ({file_size}MB) but keep getting the error: '{error_msg}'. Files under 10MB upload fine, but anything larger fails.", + ], + }, + # Account issues + { + "category": TicketCategory.ACCOUNT, + "subjects": [ + "Cannot reset password", + "Email verification not working", + "Account locked after failed login", + "Want to change email address", + "Two-factor authentication issues", + ], + "descriptions": [ + "I requested a password reset 3 times but haven't received any emails. I've checked spam and all folders. My email is {email}.", + "I created a new account but never received the verification email. I've tried resending it multiple times. Without verification, I can't access premium features.", + "My account was locked after I entered the wrong password too many times. I can now remember my correct password but the unlock email link isn't working.", + "I need to change my account email from {old_email} to {new_email} because I no longer have access to my old email. How can I do this?", + "I enabled two-factor authentication but lost my phone. I have my backup codes but the system won't accept them. I can't access my account at all now.", + ], + }, + # Product questions + { + "category": TicketCategory.PRODUCT, + "subjects": [ + "How to use advanced features", + "Feature request: Dark mode", + "Is there a mobile app?", + "Difference between plans", + "Integration with third-party tools", + ], + "descriptions": [ + "I upgraded to the Pro plan to access the {feature} feature, but I can't figure out how to use it. Is there documentation or a tutorial available?", + "I would love to see a dark mode option. I use the app late at night and the bright interface strains my eyes. This would be a great addition.", + "Is there a mobile app for iOS/Android? I can only find the web version and it's not very mobile-friendly. Would really help my workflow.", + "What's the difference between the Standard and Premium plans? The pricing page mentions 'advanced analytics' but doesn't explain what that includes.", + "Does your product integrate with {tool_name}? I use it for {workflow} and would love to connect the two. If not, are there plans to add this integration?", + ], + }, + # Shipping/delivery + { + "category": TicketCategory.SHIPPING, + "subjects": [ + "Order hasn't arrived yet", + "Wrong item delivered", + "Package damaged during shipping", + "Need to change delivery address", + "Tracking number not working", + ], + "descriptions": [ + "I ordered {product} on {date1} (Order #{order_id}). Tracking shows it was delivered on {date2}, but I never received it. Can you investigate?", + "I received my order #{order_id} today but it's the wrong item. I ordered {ordered_item} but received {received_item} instead. How do we fix this?", + "My package (Order #{order_id}) arrived today but the box was badly damaged and the product inside is broken. I need a replacement.", + "I need to change the delivery address for Order #{order_id}. It hasn't shipped yet according to tracking. Can you update it to {new_address}?", + "The tracking number {tracking_num} you sent me doesn't work on the carrier's website. It says 'invalid tracking number.' Can you verify?", + ], + }, + ] + + @staticmethod + def generate_ticket(ticket_id: int = None) -> Ticket: + """Generate a single realistic ticket""" + template = random.choice(TicketGenerator.TICKET_TEMPLATES) + category = template["category"] + + # Random values for placeholder substitution + date1 = (datetime.now() - timedelta(days=random.randint(1, 30))).strftime("%Y-%m-%d") + date2 = (datetime.now() - timedelta(days=random.randint(1, 15))).strftime("%Y-%m-%d") + amount = random.choice([9.99, 19.99, 29.99, 49.99, 99.99]) + + subject = random.choice(template["subjects"]) + description_template = random.choice(template["descriptions"]) + + # Replace placeholders + description = description_template.format( + date1=date1, + date2=date2, + amount=amount, + email=f"customer{random.randint(1000, 9999)}@example.com", + old_email=f"old{random.randint(100, 999)}@example.com", + new_email=f"new{random.randint(100, 999)}@example.com", + version=f"v{random.randint(1, 5)}.{random.randint(0, 9)}.{random.randint(0, 20)}", + device=random.choice(["iPhone 14", "Samsung Galaxy S23", "iPad Pro", "MacBook Pro"]), + os=random.choice(["iOS 17", "Android 14", "macOS 14.2", "Windows 11"]), + error_msg=random.choice( + ["ERR_CONNECTION_TIMEOUT", "FILE_TOO_LARGE", "INVALID_FORMAT", "PERMISSION_DENIED"] + ), + file_type=random.choice(["PDF", "DOCX", "PNG", "CSV"]), + file_size=random.randint(15, 100), + feature=random.choice( + ["analytics dashboard", "bulk import", "API access", "custom reports"] + ), + tool_name=random.choice(["Slack", "Salesforce", "Zapier", "Google Sheets"]), + workflow=random.choice(["project management", "customer tracking", "data analysis"]), + product=random.choice( + ["Wireless Headphones", "Smart Watch", "Laptop Stand", "USB-C Hub"] + ), + order_id=f"ORD-{random.randint(10000, 99999)}", + ordered_item=random.choice(["Blue Widget Pro", "Red Gadget Plus", "Green Device Max"]), + received_item=random.choice( + ["Yellow Widget Lite", "Purple Gadget Basic", "Orange Device Mini"] + ), + new_address="456 New St, Different City, ST 12345", + tracking_num=f"1Z{random.randint(100000000000, 999999999999)}", + ) + + ticket_id_str = f"TICKET-{ticket_id if ticket_id else random.randint(1000, 9999)}" + + return Ticket( + id=ticket_id_str, + customer_name=f"{random.choice(['John', 'Jane', 'Alex', 'Sam', 'Chris', 'Morgan'])} {random.choice(['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Davis'])}", + customer_email=f"customer{random.randint(1000, 9999)}@example.com", + subject=subject, + description=description, + category=category, + priority=None, # To be determined by agent + status=TicketStatus.NEW, + created_at=datetime.now() - timedelta(hours=random.randint(0, 48)), + ) + + @staticmethod + def generate_batch(count: int = 25) -> list[Ticket]: + """Generate a batch of tickets""" + return [TicketGenerator.generate_ticket(i + 1) for i in range(count)] + + +# Knowledge base mock data +KNOWLEDGE_BASE = { + "billing": { + "refund_policy": "Refunds are processed within 5-7 business days for valid cancellations. Pro-rated refunds available for annual plans.", + "payment_methods": "We accept Visa, Mastercard, American Express, and PayPal. Update payment methods in Account Settings.", + "billing_cycle": "Billing occurs on the same date each month/year from your original signup date.", + }, + "technical": { + "common_errors": { + "ERR_CONNECTION_TIMEOUT": "Check internet connection and firewall settings. Try disabling VPN.", + "FILE_TOO_LARGE": "Maximum upload size is 100MB per file. Compress or split larger files.", + "INVALID_FORMAT": "Supported formats: PDF, DOCX, PNG, JPG, CSV. Check file extension.", + }, + "system_requirements": "Minimum: 4GB RAM, modern browser (Chrome 90+, Firefox 88+, Safari 14+)", + "troubleshooting": "Clear cache and cookies, try incognito mode, ensure JavaScript is enabled", + }, + "account": { + "password_reset": "Password reset emails sent from noreply@support.example.com. Check spam folder. Link expires in 1 hour.", + "2fa_backup": "Each backup code can be used once. Contact support if all codes are lost with account verification.", + "email_change": "Email changes require verification of both old and new addresses for security.", + }, +} + + +# Team routing rules +TEAM_ROUTING = { + TicketCategory.BILLING: "billing-team", + TicketCategory.TECHNICAL: "tech-support", + TicketCategory.ACCOUNT: "account-services", + TicketCategory.PRODUCT: "product-success", + TicketCategory.SHIPPING: "logistics-team", +} + + +# Priority rules +def determine_priority(ticket: Ticket) -> TicketPriority: + """Simple priority determination rules""" + urgent_keywords = ["can't access", "locked out", "lost access", "urgent", "immediately"] + high_keywords = ["not working", "broken", "error", "crashes", "failed"] + + description_lower = ticket.description.lower() + + if any(keyword in description_lower for keyword in urgent_keywords): + return TicketPriority.URGENT + elif any(keyword in description_lower for keyword in high_keywords): + return TicketPriority.HIGH + elif ticket.category == TicketCategory.BILLING: + return TicketPriority.HIGH + else: + return TicketPriority.MEDIUM + + +def process_ticket(ticket: Ticket) -> Ticket: + """Process a ticket: assign priority and team""" + ticket.priority = determine_priority(ticket) + if ticket.category: + ticket.assigned_team = TEAM_ROUTING.get(ticket.category, "general-support") + else: + ticket.assigned_team = "general-support" + return ticket + + +def main(): + """Demonstrate the customer service ticket system""" + print("=" * 80) + print("Customer Support Ticket System - Demo") + print("=" * 80) + print() + + # Generate batch of tickets + num_tickets = 50 + print(f"Generating {num_tickets} support tickets...") + tickets = TicketGenerator.generate_batch(num_tickets) + print(f"✓ Generated {len(tickets)} tickets\n") + + # Process tickets + print("Processing tickets (categorization, prioritization, routing)...") + processed_tickets = [process_ticket(ticket) for ticket in tickets] + print(f"✓ Processed {len(processed_tickets)} tickets\n") + + # Summary statistics + print("=" * 80) + print("Summary Statistics") + print("=" * 80) + + category_counts = {} + priority_counts = {} + team_counts = {} + + for ticket in processed_tickets: + if ticket.category: + category_counts[ticket.category.value] = ( + category_counts.get(ticket.category.value, 0) + 1 + ) + if ticket.priority: + priority_counts[ticket.priority.value] = ( + priority_counts.get(ticket.priority.value, 0) + 1 + ) + if ticket.assigned_team: + team_counts[ticket.assigned_team] = team_counts.get(ticket.assigned_team, 0) + 1 + + print("\nBy Category:") + for category, count in sorted(category_counts.items()): + print(f" {category.capitalize()}: {count}") + + print("\nBy Priority:") + for priority, count in sorted( + priority_counts.items(), key=lambda x: ["low", "medium", "high", "urgent"].index(x[0]) + ): + print(f" {priority.capitalize()}: {count}") + + print("\nBy Team:") + for team, count in sorted(team_counts.items()): + print(f" {team}: {count}") + + # Display sample tickets + print("\n" + "=" * 80) + print("Sample Tickets") + print("=" * 80) + + for i, ticket in enumerate(processed_tickets[:10], 1): + print(f"\nTicket #{i}") + print(f" ID: {ticket.id}") + print(f" Customer: {ticket.customer_name} ({ticket.customer_email})") + print(f" Subject: {ticket.subject}") + print(f" Category: {ticket.category.value if ticket.category else 'unclassified'}") + print(f" Priority: {ticket.priority.value if ticket.priority else 'unset'}") + print(f" Status: {ticket.status.value}") + print(f" Assigned Team: {ticket.assigned_team or 'unassigned'}") + print( + f" Created: {ticket.created_at.strftime('%Y-%m-%d %H:%M:%S') if ticket.created_at else 'unknown'}" + ) + print(f" Description: {ticket.description[:100]}...") + + print("\n" + "=" * 80) + print(f"Demo complete! {len(processed_tickets)} tickets ready for processing.") + print("=" * 80) + + return processed_tickets + + +if __name__ == "__main__": + main() diff --git a/tool_use/utils/customer_service_tools.py b/tool_use/utils/customer_service_tools.py new file mode 100644 index 00000000..098192aa --- /dev/null +++ b/tool_use/utils/customer_service_tools.py @@ -0,0 +1,350 @@ +""" +Customer Service Tools for Claude +Implements tool functions for processing support tickets +""" + +import json +from typing import Literal + +from anthropic import beta_tool + +from .customer_service_api import ( + KNOWLEDGE_BASE, + TEAM_ROUTING, + Ticket, + TicketCategory, + TicketGenerator, + TicketPriority, + TicketStatus, +) + +# Global state for demo purposes - in production this would be a database +_ticket_queue: list[Ticket] = [] +_current_tickets: dict[str, Ticket] = {} +_queue_index = 0 + + +def initialize_ticket_queue(count: int = 25): + """Initialize the ticket queue with generated tickets""" + global _ticket_queue, _queue_index, _current_tickets + _ticket_queue = TicketGenerator.generate_batch(count) + _queue_index = 0 + _current_tickets = {} + + +def _get_ticket(ticket_id: str) -> Ticket | None: + """Helper to retrieve a ticket by ID""" + return _current_tickets.get(ticket_id) + + +def _serialize_ticket(ticket: Ticket) -> str: + """Convert ticket to JSON string""" + return json.dumps( + { + "id": ticket.id, + "customer_name": ticket.customer_name, + "customer_email": ticket.customer_email, + "subject": ticket.subject, + "description": ticket.description, + "category": ticket.category.value if ticket.category else None, + "priority": ticket.priority.value if ticket.priority else None, + "status": ticket.status.value, + "created_at": ticket.created_at.isoformat() if ticket.created_at else None, + "assigned_team": ticket.assigned_team, + "notes": ticket.notes, + }, + indent=2, + ) + + +@beta_tool +def get_next_ticket() -> str: + """ + Get the next unprocessed ticket from the queue. + Returns ticket details as JSON string. + """ + global _queue_index + + if _queue_index >= len(_ticket_queue): + return json.dumps( + { + "error": "No more tickets in queue", + "processed": _queue_index, + "total": len(_ticket_queue), + } + ) + + ticket = _ticket_queue[_queue_index] + _queue_index += 1 + _current_tickets[ticket.id] = ticket + + return _serialize_ticket(ticket) + + +@beta_tool +def classify_ticket( + ticket_id: str, category: Literal["billing", "technical", "account", "product", "shipping"] +) -> str: + """ + Classify a ticket into a category. + + Args: + ticket_id: The ticket ID + category: The category to assign + + Returns: + Confirmation message + """ + ticket = _get_ticket(ticket_id) + if not ticket: + return json.dumps({"error": f"Ticket {ticket_id} not found"}) + + ticket.category = TicketCategory(category) + + return json.dumps( + { + "success": True, + "message": f"Ticket {ticket_id} classified as {category}", + "ticket_id": ticket_id, + } + ) + + +@beta_tool +def search_knowledge_base(category: str, query: str) -> str: + """ + Search the knowledge base for relevant information. + + Args: + category: The category to search (billing, technical, account) + query: Keywords to search for + + Returns: + Relevant knowledge base articles as JSON + """ + category_lower = category.lower() + + if category_lower not in KNOWLEDGE_BASE: + return json.dumps( + { + "error": f"Category '{category}' not found", + "available_categories": list(KNOWLEDGE_BASE.keys()), + } + ) + + category_kb = KNOWLEDGE_BASE[category_lower] + + # Simple keyword search + query_lower = query.lower() + results = {} + + for key, value in category_kb.items(): + if isinstance(value, dict): + # Search nested dictionaries + for sub_key, sub_value in value.items(): + if query_lower in sub_key.lower() or query_lower in str(sub_value).lower(): + if key not in results: + results[key] = {} + results[key][sub_key] = sub_value + else: + # Search flat key-value pairs + if query_lower in key.lower() or query_lower in value.lower(): + results[key] = value + + return json.dumps( + { + "category": category, + "query": query, + "results": results if results else category_kb, + "all_available": category_kb, + }, + indent=2, + ) + + +@beta_tool +def set_priority(ticket_id: str, priority: Literal["low", "medium", "high", "urgent"]) -> str: + """ + Set the priority level for a ticket. + + Args: + ticket_id: The ticket ID + priority: Priority level + + Returns: + Confirmation message + """ + ticket = _get_ticket(ticket_id) + if not ticket: + return json.dumps({"error": f"Ticket {ticket_id} not found"}) + + old_priority = ticket.priority.value if ticket.priority else "unset" + ticket.priority = TicketPriority(priority) + + return json.dumps( + { + "success": True, + "message": f"Ticket {ticket_id} priority updated from {old_priority} to {priority}", + "ticket_id": ticket_id, + "old_priority": old_priority, + "new_priority": priority, + } + ) + + +@beta_tool +def route_to_team(ticket_id: str, team: str) -> str: + """ + Route a ticket to the appropriate support team. + + Args: + ticket_id: The ticket ID + team: Team name (billing-team, tech-support, account-services, product-success, logistics-team) + + Returns: + Confirmation message + """ + ticket = _get_ticket(ticket_id) + if not ticket: + return json.dumps({"error": f"Ticket {ticket_id} not found"}) + + valid_teams = list(TEAM_ROUTING.values()) + if team not in valid_teams: + return json.dumps( + { + "error": f"Invalid team '{team}'", + "valid_teams": valid_teams, + } + ) + + old_team = ticket.assigned_team + ticket.assigned_team = team + ticket.status = TicketStatus.OPEN + + return json.dumps( + { + "success": True, + "message": f"Ticket {ticket_id} routed to {team}", + "ticket_id": ticket_id, + "old_team": old_team, + "new_team": team, + } + ) + + +@beta_tool +def draft_response(ticket_id: str, response: str) -> str: + """ + Draft a response to the customer. + + Args: + ticket_id: The ticket ID + response: The draft response text + + Returns: + Confirmation that draft was saved + """ + ticket = _get_ticket(ticket_id) + if not ticket: + return json.dumps({"error": f"Ticket {ticket_id} not found"}) + + # Store draft in notes with special prefix + draft_note = f"[DRAFT RESPONSE] {response}" + ticket.notes.append(draft_note) + + return json.dumps( + { + "success": True, + "message": f"Draft response saved for ticket {ticket_id}", + "ticket_id": ticket_id, + "draft_length": len(response), + } + ) + + +@beta_tool +def add_note(ticket_id: str, note: str) -> str: + """ + Add an internal note to the ticket. + + Args: + ticket_id: The ticket ID + note: Internal note for team reference + + Returns: + Confirmation message + """ + ticket = _get_ticket(ticket_id) + if not ticket: + return json.dumps({"error": f"Ticket {ticket_id} not found"}) + + ticket.notes.append(note) + + return json.dumps( + { + "success": True, + "message": f"Note added to ticket {ticket_id}", + "ticket_id": ticket_id, + "total_notes": len(ticket.notes), + } + ) + + +@beta_tool +def mark_complete(ticket_id: str) -> str: + """ + Mark ticket as processed and ready for team review. + + Args: + ticket_id: The ticket ID + + Returns: + Confirmation and summary of ticket processing + """ + ticket = _get_ticket(ticket_id) + if not ticket: + return json.dumps({"error": f"Ticket {ticket_id} not found"}) + + # Validate ticket is ready for completion + if not ticket.category: + return json.dumps({"error": "Cannot complete ticket without category classification"}) + + if not ticket.priority: + return json.dumps({"error": "Cannot complete ticket without priority assignment"}) + + if not ticket.assigned_team: + return json.dumps({"error": "Cannot complete ticket without team routing"}) + + ticket.status = TicketStatus.RESOLVED + + summary = { + "success": True, + "message": f"Ticket {ticket_id} marked complete", + "ticket_id": ticket_id, + "summary": { + "customer": ticket.customer_name, + "subject": ticket.subject, + "category": ticket.category.value, + "priority": ticket.priority.value, + "assigned_team": ticket.assigned_team, + "total_notes": len(ticket.notes), + "status": ticket.status.value, + }, + } + + return json.dumps(summary, indent=2) + + +# Helper function for demo/testing +def get_all_tools(): + """Return all tool functions for registration with Anthropic API""" + return [ + get_next_ticket, + classify_ticket, + search_knowledge_base, + set_priority, + route_to_team, + draft_response, + add_note, + mark_complete, + ] diff --git a/tool_use/utils/team_expense_api.py b/tool_use/utils/team_expense_api.py new file mode 100644 index 00000000..c3854ac9 --- /dev/null +++ b/tool_use/utils/team_expense_api.py @@ -0,0 +1,581 @@ +""" +Example Mock API for Team Expense Management Demo + +This is a domain-specific API used in the programmatic tool calling cookbook. +It provides mock tools for retrieving team member information, expense records, +and budget limits by employee level. +""" + +import json +import random +import time +from datetime import datetime, timedelta + +# Configuration +EXPENSE_LINE_ITEMS_PER_PERSON_MIN = 20 +EXPENSE_LINE_ITEMS_PER_PERSON_MAX = 50 +DELAY_MULTIPLIER = 0 # Adjust this to simulate API latency + + +def get_team_members(department: str) -> str: + """Returns a list of team members for a given department. + + Each team member includes their ID, name, role, level, and contact information. + Use this to get a list of people whose expenses you want to analyze. + + Args: + department: The department name (e.g., 'engineering', 'sales', 'marketing'). + Case-insensitive. + + Returns: + JSON string containing an array of team member objects with fields: + - id: Unique employee identifier + - name: Full name + - role: Job title + - level: Employee level (junior, mid, senior, staff, principal) + - email: Contact email + - department: Department name + """ + import time + + time.sleep(DELAY_MULTIPLIER * 0.1) + + department = department.lower() + + # Mock team data by department + teams = { + "engineering": [ + { + "id": "ENG001", + "name": "Alice Chen", + "role": "Senior Software Engineer", + "level": "senior", + "email": "alice.chen@company.com", + "department": "engineering", + }, + { + "id": "ENG002", + "name": "Bob Martinez", + "role": "Staff Engineer", + "level": "staff", + "email": "bob.martinez@company.com", + "department": "engineering", + }, + { + "id": "ENG003", + "name": "Carol White", + "role": "Software Engineer", + "level": "mid", + "email": "carol.white@company.com", + "department": "engineering", + }, + { + "id": "ENG004", + "name": "David Kim", + "role": "Principal Engineer", + "level": "principal", + "email": "david.kim@company.com", + "department": "engineering", + }, + { + "id": "ENG005", + "name": "Emma Johnson", + "role": "Junior Software Engineer", + "level": "junior", + "email": "emma.johnson@company.com", + "department": "engineering", + }, + { + "id": "ENG006", + "name": "Frank Liu", + "role": "Senior Software Engineer", + "level": "senior", + "email": "frank.liu@company.com", + "department": "engineering", + }, + { + "id": "ENG007", + "name": "Grace Taylor", + "role": "Software Engineer", + "level": "mid", + "email": "grace.taylor@company.com", + "department": "engineering", + }, + { + "id": "ENG008", + "name": "Henry Park", + "role": "Staff Engineer", + "level": "staff", + "email": "henry.park@company.com", + "department": "engineering", + }, + ], + "sales": [ + { + "id": "SAL001", + "name": "Irene Davis", + "role": "Account Executive", + "level": "mid", + "email": "irene.davis@company.com", + "department": "sales", + }, + { + "id": "SAL002", + "name": "Jack Wilson", + "role": "Senior Account Executive", + "level": "senior", + "email": "jack.wilson@company.com", + "department": "sales", + }, + { + "id": "SAL003", + "name": "Kelly Brown", + "role": "Sales Development Rep", + "level": "junior", + "email": "kelly.brown@company.com", + "department": "sales", + }, + { + "id": "SAL004", + "name": "Leo Garcia", + "role": "Regional Sales Director", + "level": "staff", + "email": "leo.garcia@company.com", + "department": "sales", + }, + { + "id": "SAL005", + "name": "Maya Patel", + "role": "Account Executive", + "level": "mid", + "email": "maya.patel@company.com", + "department": "sales", + }, + { + "id": "SAL006", + "name": "Nathan Scott", + "role": "VP of Sales", + "level": "principal", + "email": "nathan.scott@company.com", + "department": "sales", + }, + ], + "marketing": [ + { + "id": "MKT001", + "name": "Olivia Thompson", + "role": "Marketing Manager", + "level": "senior", + "email": "olivia.thompson@company.com", + "department": "marketing", + }, + { + "id": "MKT002", + "name": "Peter Anderson", + "role": "Content Specialist", + "level": "mid", + "email": "peter.anderson@company.com", + "department": "marketing", + }, + { + "id": "MKT003", + "name": "Quinn Rodriguez", + "role": "Marketing Coordinator", + "level": "junior", + "email": "quinn.rodriguez@company.com", + "department": "marketing", + }, + { + "id": "MKT004", + "name": "Rachel Lee", + "role": "Director of Marketing", + "level": "staff", + "email": "rachel.lee@company.com", + "department": "marketing", + }, + { + "id": "MKT005", + "name": "Sam Miller", + "role": "Social Media Manager", + "level": "mid", + "email": "sam.miller@company.com", + "department": "marketing", + }, + ], + } + + if department not in teams: + return json.dumps( + { + "error": f"Department '{department}' not found. Available departments: {', '.join(teams.keys())}" + } + ) + + return json.dumps(teams[department], indent=2) + + +def get_expenses(employee_id: str, quarter: str) -> str: + """Returns all expense line items for a given employee in a specific quarter. + + Each expense includes comprehensive metadata: date, category, description, amount, + receipt details, approval chain, merchant information, and more. An employee may + have anywhere from a few to 150+ expense line items per quarter, and each line + item contains substantial metadata for audit and compliance purposes. + + Args: + employee_id: The unique employee identifier (e.g., 'ENG001', 'SAL002') + quarter: Quarter identifier (e.g., 'Q1', 'Q2', 'Q3', 'Q4') + + Returns: + JSON string containing an array of expense objects with fields: + - expense_id: Unique expense identifier + - date: ISO format date when expense occurred + - category: Expense type (travel, meals, lodging, software, equipment, etc.) + - description: Details about the expense + - amount: Dollar amount (float) + - currency: Currency code (default 'USD') + - status: Approval status (approved, pending, rejected) + - receipt_url: URL to uploaded receipt image + - approved_by: Manager or finance person who approved + - store_name: Merchant or vendor name + - store_location: City and state of merchant + - reimbursement_date: When the expense was reimbursed (if applicable) + - payment_method: How it was paid (corporate_card, personal_reimbursement) + - project_code: Project or cost center code + - notes: Employee justification or additional context + """ + + time.sleep(DELAY_MULTIPLIER * 0.2) + + # Generate a deterministic but varied number of expenses based on employee_id + random.seed(hash(employee_id + quarter)) + num_expenses = random.randint( + EXPENSE_LINE_ITEMS_PER_PERSON_MIN, EXPENSE_LINE_ITEMS_PER_PERSON_MAX + ) + + # Quarter date ranges + quarter_dates = { + "Q1": (datetime(2024, 1, 1), datetime(2024, 3, 31)), + "Q2": (datetime(2024, 4, 1), datetime(2024, 6, 30)), + "Q3": (datetime(2024, 7, 1), datetime(2024, 9, 30)), + "Q4": (datetime(2024, 10, 1), datetime(2024, 12, 31)), + } + + if quarter.upper() not in quarter_dates: + return json.dumps({"error": f"Invalid quarter '{quarter}'. Must be Q1, Q2, Q3, or Q4"}) + + start_date, end_date = quarter_dates[quarter.upper()] + + # Expense categories and typical amounts + expense_categories = [ + ("travel", "Flight to client meeting", 400, 1500), + ("travel", "Train ticket", 1000, 1500), + ("travel", "Rental car", 1000, 1500), + ("travel", "Taxi/Uber", 150, 200), + ("travel", "Parking fee", 10, 50), + ("lodging", "Hotel stay", 150, 1900), + ("lodging", "Airbnb rental", 1000, 1950), + ("meals", "Client dinner", 50, 250), + ("meals", "Team lunch", 20, 100), + ("meals", "Conference breakfast", 15, 40), + ("meals", "Coffee meeting", 5, 25), + ("software", "SaaS subscription", 10, 200), + ("software", "API credits", 50, 500), + ("equipment", "Monitor", 200, 800), + ("equipment", "Keyboard", 50, 200), + ("equipment", "Webcam", 50, 150), + ("equipment", "Headphones", 100, 300), + ("conference", "Conference ticket", 500, 2500), + ("conference", "Workshop registration", 200, 1000), + ("office", "Office supplies", 10, 100), + ("office", "Books", 20, 80), + ("internet", "Mobile data", 30, 100), + ("internet", "WiFi hotspot", 20, 60), + ] + + # Manager names for approvals + managers = [ + "Sarah Johnson", + "Michael Chen", + "Emily Rodriguez", + "David Park", + "Jennifer Martinez", + ] + + # Store/merchant names by category + merchants = { + "travel": [ + "United Airlines", + "Delta", + "American Airlines", + "Southwest", + "Enterprise Rent-A-Car", + ], + "lodging": ["Marriott", "Hilton", "Hyatt", "Airbnb", "Holiday Inn"], + "meals": ["Olive Garden", "Starbucks", "The Capital Grille", "Chipotle", "Panera Bread"], + "software": ["AWS", "GitHub", "Linear", "Notion", "Figma"], + "equipment": ["Amazon", "Best Buy", "Apple Store", "B&H Photo", "Newegg"], + "conference": ["EventBrite", "WWDC", "AWS re:Invent", "Google I/O", "ReactConf"], + "office": ["Staples", "Office Depot", "Amazon", "Target"], + "internet": ["Verizon", "AT&T", "T-Mobile", "Comcast"], + } + + # US cities for store locations + cities = [ + "San Francisco, CA", + "New York, NY", + "Austin, TX", + "Seattle, WA", + "Boston, MA", + "Chicago, IL", + "Denver, CO", + "Los Angeles, CA", + "Portland, OR", + "Miami, FL", + ] + + # Project codes + project_codes = [ + "PROJ-1001", + "PROJ-1002", + "PROJ-2001", + "DEPT-ENG", + "DEPT-OPS", + "CLIENT-A", + "CLIENT-B", + ] + + # Justification templates + justifications = { + "travel": [ + "Client meeting to discuss Q4 roadmap and requirements", + "On-site visit for infrastructure review and planning", + "Conference attendance for professional development", + "Team offsite for strategic planning session", + "Customer presentation and product demo", + ], + "lodging": [ + "Hotel for multi-day client visit", + "Accommodation during conference attendance", + "Extended stay for project implementation", + "Lodging for team collaboration week", + ], + "meals": [ + "Client dinner discussing partnership opportunities", + "Team lunch during sprint planning", + "Breakfast meeting with stakeholders", + "Working dinner during crunch period", + ], + "software": [ + "Required tool for development workflow", + "API credits for production workload", + "Team collaboration platform subscription", + "Design and prototyping tool license", + ], + "equipment": [ + "Replacing failed hardware", + "Upgraded monitor for productivity", + "Required for remote work setup", + "Better equipment for video calls", + ], + "conference": [ + "Professional development - learning new technologies", + "Networking with industry leaders and potential partners", + "Presenting company work at industry event", + "Training workshop for certification", + ], + "office": [ + "Supplies for home office setup", + "Reference materials for project work", + "Team whiteboarding supplies", + ], + "internet": [ + "Mobile hotspot for reliable connectivity", + "Upgraded internet for remote work", + "International data plan for travel", + ], + } + + expenses = [] + for i in range(num_expenses): + category, desc_template, min_amt, max_amt = random.choice(expense_categories) + + # Generate random date within quarter + days_diff = (end_date - start_date).days + random_days = random.randint(0, days_diff) + expense_date = start_date + timedelta(days=random_days) + + # Generate amount + amount = round(random.uniform(min_amt, max_amt), 2) + + # Status (most are approved) + status = random.choices(["approved", "pending", "rejected"], weights=[0.85, 0.10, 0.05])[0] + + # Generate additional metadata + approved_by = random.choice(managers) if status == "approved" else None + store_name = random.choice(merchants.get(category, ["Unknown Merchant"])) + store_location = random.choice(cities) + payment_method = random.choice(["corporate_card", "personal_reimbursement"]) + project_code = random.choice(project_codes) + notes = random.choice(justifications.get(category, ["Business expense"])) + + # Reimbursement date is 15-30 days after expense date for approved expenses + reimbursement_date = None + if status == "approved" and payment_method == "personal_reimbursement": + reimb_days = random.randint(15, 30) + reimbursement_date = (expense_date + timedelta(days=reimb_days)).strftime("%Y-%m-%d") + + expenses.append( + { + "expense_id": f"{employee_id}_{quarter}_{i:03d}", + "date": expense_date.strftime("%Y-%m-%d"), + "category": category, + "description": desc_template, + "amount": amount, + "currency": "USD", + "status": status, + "receipt_url": f"https://receipts.company.com/{employee_id}/{quarter}/{i:03d}.pdf", + "approved_by": approved_by, + "store_name": store_name, + "store_location": store_location, + "reimbursement_date": reimbursement_date, + "payment_method": payment_method, + "project_code": project_code, + "notes": notes, + } + ) + + # Sort by date + expenses.sort(key=lambda x: x["date"]) + + return json.dumps(expenses, indent=2) + + +def get_custom_budget(user_id: str) -> str: + """Get the custom quarterly travel budget for a specific employee. + + Most employees have a standard $5,000 quarterly travel budget. However, some + employees have custom budget exceptions based on their role requirements. + This function checks if a specific employee has a custom budget assigned. + + Args: + user_id: The unique employee identifier (e.g., 'ENG001', 'SAL002') + + Returns: + JSON string containing: + - user_id: Employee identifier + - has_custom_budget: Boolean indicating if custom budget exists + - travel_budget: Quarterly travel budget amount (custom or standard $5,000) + - reason: Explanation for custom budget (if applicable) + - currency: Currency code (default 'USD') + """ + time.sleep(DELAY_MULTIPLIER * 0.05) + + # Employees with custom budget exceptions + custom_budgets = { + "ENG002": { + "user_id": "ENG002", + "has_custom_budget": True, + "travel_budget": 8000, + "reason": "Staff engineer with regular client site visits", + "currency": "USD", + }, + "ENG004": { + "user_id": "ENG004", + "has_custom_budget": True, + "travel_budget": 12000, + "reason": "Principal engineer leading distributed team across multiple offices", + "currency": "USD", + }, + "SAL004": { + "user_id": "SAL004", + "has_custom_budget": True, + "travel_budget": 15000, + "reason": "Regional sales director covering west coast territory", + "currency": "USD", + }, + "SAL006": { + "user_id": "SAL006", + "has_custom_budget": True, + "travel_budget": 20000, + "reason": "VP of Sales with extensive client travel requirements", + "currency": "USD", + }, + "MKT004": { + "user_id": "MKT004", + "has_custom_budget": True, + "travel_budget": 10000, + "reason": "Director of Marketing attending industry conferences and partner meetings", + "currency": "USD", + }, + } + + # Check if user has custom budget + if user_id in custom_budgets: + return json.dumps(custom_budgets[user_id], indent=2) + + # Return standard budget + return json.dumps( + { + "user_id": user_id, + "has_custom_budget": False, + "travel_budget": 5000, + "reason": "Standard quarterly travel budget", + "currency": "USD", + }, + indent=2, + ) + + +# Helper function to get all available tools +def get_expense_tools(): + """Returns a list of all expense management tools for use with Claude API.""" + return [get_team_members, get_expenses, get_custom_budget] + + +if __name__ == "__main__": + # Example usage demonstrating custom budget checking + print("=== Team Expense Analysis Example ===\n") + + # Get team members + team = json.loads(get_team_members("engineering")) + + exceeded_standard = [] + for member in team[:5]: # Just check first 5 for demo + print(f"Checking expenses for {member['name']}...") + + # Fetch this person's expenses (could be 100+ line items) + expenses = json.loads(get_expenses(member["id"], "Q3")) + + # Calculate total travel expenses + travel_total = sum( + exp["amount"] + for exp in expenses + if exp["status"] == "approved" and exp["category"] in ["travel", "lodging"] + ) + + print(f" - Found {len(expenses)} expense line items") + print(f" - Total approved travel expenses: ${travel_total:,.2f}") + + # Check against standard $5,000 budget + if travel_total > 5000: + print(" ⚠️ Exceeded standard $5,000 budget") + # Now check if they have a custom budget exception + custom = json.loads(get_custom_budget(member["id"])) + print(f" - Custom budget: ${custom['travel_budget']:,}") + + if travel_total > custom["travel_budget"]: + print(" ❌ VIOLATION: Exceeded custom budget!") + exceeded_standard.append( + { + "name": member["name"], + "spent": travel_total, + "custom_limit": custom["travel_budget"], + } + ) + else: + print(" ✅ Within custom budget limit") + print() + + print("\n=== Summary: Employees Over Custom Budget ===") + print(json.dumps(exceeded_standard, indent=2)) diff --git a/tool_use/utils/visualize.py b/tool_use/utils/visualize.py new file mode 100644 index 00000000..7d33d50e --- /dev/null +++ b/tool_use/utils/visualize.py @@ -0,0 +1,373 @@ +""" +Standalone Claude API Response Visualizer +""" + +import json +from typing import Any + +from rich.console import Console +from rich.panel import Panel +from rich.syntax import Syntax +from rich.text import Text +from rich.tree import Tree + + +class ParsedContent: + """Represents a parsed content block from a Claude message.""" + + def __init__(self, content_type: str, data: dict[str, Any]): + self.type = content_type + self.data = data + + +class ParsedMessage: + """Represents a parsed Claude message with metadata.""" + + def __init__( + self, + role: str, + content: list[ParsedContent], + model: str | None = None, + stop_reason: str | None = None, + usage: dict[str, int] | None = None, + ): + self.role = role + self.content = content + self.model = model + self.stop_reason = stop_reason + self.usage = usage or {} + + +def parse_content_block(block: dict[str, Any] | Any) -> ParsedContent: + """Parse a single content block from a message.""" + # Handle dict format (from JSON) + if isinstance(block, dict): + content_type = block.get("type", "unknown") + return ParsedContent(content_type, block) + + # Handle Anthropic SDK objects + if hasattr(block, "type"): + content_type = block.type + # Convert to dict for easier access + if hasattr(block, "model_dump"): + data = block.model_dump() + elif hasattr(block, "dict"): + data = block.dict() + else: + data = {"raw": str(block)} + return ParsedContent(content_type, data) + + # Fallback for text strings + if isinstance(block, str): + return ParsedContent("text", {"text": block}) + + return ParsedContent("unknown", {"raw": str(block)}) + + +def parse_response(response: dict[str, Any] | Any) -> ParsedMessage: + """Parse a Claude API response into a structured format.""" + # Handle dict format (from JSON) + if isinstance(response, dict): + role = response.get("role", "unknown") + content_blocks = response.get("content", []) + model = response.get("model") + stop_reason = response.get("stop_reason") + usage = response.get("usage", {}) + + parsed_content = [parse_content_block(block) for block in content_blocks] + + return ParsedMessage( + role=role, + content=parsed_content, + model=model, + stop_reason=stop_reason, + usage=usage, + ) + + # Handle Anthropic SDK Message object + if hasattr(response, "content"): + role = getattr(response, "role", "unknown") + content_blocks = response.content + model = getattr(response, "model", None) + stop_reason = getattr(response, "stop_reason", None) + + # Extract usage stats + usage = {} + if hasattr(response, "usage"): + usage_obj = response.usage + if hasattr(usage_obj, "input_tokens"): + usage["input_tokens"] = usage_obj.input_tokens + if hasattr(usage_obj, "output_tokens"): + usage["output_tokens"] = usage_obj.output_tokens + + parsed_content = [parse_content_block(block) for block in content_blocks] + + return ParsedMessage( + role=role, + content=parsed_content, + model=model, + stop_reason=stop_reason, + usage=usage, + ) + + raise ValueError(f"Unsupported response type: {type(response)}") + + +def format_json(data: Any, max_length: int = 500) -> str: + """Format data as JSON string, truncating if too long.""" + json_str = json.dumps(data, indent=2) + if len(json_str) > max_length: + json_str = json_str[:max_length] + "\n ... (truncated)" + return json_str + + +def render_text_content(content: ParsedContent, tree: Tree) -> None: + """Render a text content block.""" + text = content.data.get("text", "") + if text: + # Truncate very long text + if len(text) > 1000: + text = text[:1000] + "\n... (truncated)" + text_node = tree.add("[cyan]Text[/cyan]") + text_node.add(Text(text, style="white")) + + +def render_tool_use(content: ParsedContent, tree: Tree) -> None: + """Render a tool_use content block.""" + tool_name = content.data.get("name", "unknown") + tool_id = content.data.get("id", "") + tool_input = content.data.get("input", {}) + caller = content.data.get("caller", {}) + + tool_node = tree.add(f"[yellow]Tool Use:[/yellow] [bold yellow]{tool_name}[/bold yellow]") + + if tool_id: + tool_node.add(f"[dim white]ID:[/dim white] {tool_id}") + + # Show caller type if available + if caller: + caller_type = caller.get("type", "unknown") + if caller_type == "code_execution_20250825": + caller_label = "code execution environment" + elif caller_type == "direct": + caller_label = "model (direct)" + else: + caller_label = caller_type + tool_node.add(f"[dim white]Caller:[/dim white] {caller_label}") + + if tool_input: + input_node = tool_node.add("[green]Input:[/green]") + json_syntax = Syntax(format_json(tool_input), "json", theme="monokai", line_numbers=False) + input_node.add(json_syntax) + + +def render_server_tool_use(content: ParsedContent, tree: Tree) -> None: + """Render a server_tool_use content block.""" + tool_id = content.data.get("id", "") + tool_input = content.data.get("input", {}) + caller = content.data.get("caller", {}) + + server_node = tree.add("[yellow]Server Tool Use[/yellow]") + + if tool_id: + server_node.add(f"[dim white]ID:[/dim white] {tool_id}") + + if caller: + caller_type = caller.get("type", "unknown") + server_node.add(f"[dim white]Caller:[/dim white] {caller_type}") + + # Show code from input if available + if tool_input: + code = tool_input.get("code") + if code: + code_node = server_node.add("[green]Code:[/green]") + if len(code) > 1000: + code = code[:1000] + "\n... (truncated)" + code_syntax = Syntax(code, "python", theme="monokai", line_numbers=True) + code_node.add(code_syntax) + else: + input_node = server_node.add("[green]Input:[/green]") + json_syntax = Syntax( + format_json(tool_input), "json", theme="monokai", line_numbers=False + ) + input_node.add(json_syntax) + + +def render_tool_result(content: ParsedContent, tree: Tree) -> None: + """Render a tool_result content block.""" + tool_id = content.data.get("tool_use_id", "") + is_error = content.data.get("is_error", False) + result_content = content.data.get("content", "") + + status = "[red]Error[/red]" if is_error else "[green]Success[/green]" + result_node = tree.add(f"[yellow]Tool Result:[/yellow] {status}") + + if tool_id: + result_node.add(f"[dim white]Tool Use ID:[/dim white] {tool_id}") + + if result_content: + if isinstance(result_content, list): + for item in result_content: + if isinstance(item, dict) and item.get("type") == "text": + text = item.get("text", "") + if text: + output_node = result_node.add("[cyan]Output:[/cyan]") + if len(text) > 1000: + text = text[:1000] + "\n... (truncated)" + output_node.add(Text(text, style="white")) + else: + output_node = result_node.add("[cyan]Output:[/cyan]") + output_node.add(str(item)) + else: + output_node = result_node.add("[cyan]Output:[/cyan]") + text = str(result_content) + if len(text) > 1000: + text = text[:1000] + "\n... (truncated)" + output_node.add(Text(text, style="white")) + + +def render_code_execution_result(content: ParsedContent, tree: Tree) -> None: + """Render a code_execution_tool_result content block.""" + nested_content = content.data.get("content", {}) + + if isinstance(nested_content, dict): + return_code = nested_content.get("return_code", 0) + stdout = nested_content.get("stdout", "") + stderr = nested_content.get("stderr", "") + + status = ( + f"[green]Success (exit {return_code})[/green]" + if return_code == 0 + else f"[red]Error (exit {return_code})[/red]" + ) + result_node = tree.add(f"[yellow]Code Execution Result:[/yellow] {status}") + + if stdout: + stdout_node = result_node.add("[green]stdout:[/green]") + if len(stdout) > 2000: + stdout = stdout[:2000] + "\n... (truncated)" + stdout_node.add(Text(stdout, style="white")) + + if stderr: + stderr_node = result_node.add("[red]stderr:[/red]") + if len(stderr) > 2000: + stderr = stderr[:2000] + "\n... (truncated)" + stderr_node.add(Text(stderr, style="white")) + + if not stdout and not stderr: + result_node.add("[dim white](no output)[/dim white]") + else: + result_node = tree.add("[yellow]Code Execution Result[/yellow]") + json_syntax = Syntax(format_json(content.data), "json", theme="monokai", line_numbers=False) + result_node.add(json_syntax) + + +def render_content_block(content: ParsedContent, tree: Tree) -> None: + """Render a single content block based on its type.""" + if content.type == "text": + render_text_content(content, tree) + elif content.type == "tool_use": + render_tool_use(content, tree) + elif content.type == "tool_result": + render_tool_result(content, tree) + elif content.type == "server_tool_use": + render_server_tool_use(content, tree) + elif content.type == "code_execution_tool_result": + render_code_execution_result(content, tree) + else: + # Unknown content type + unknown_node = tree.add(f"[magenta]Unknown Type:[/magenta] {content.type}") + json_syntax = Syntax(format_json(content.data), "json", theme="monokai", line_numbers=False) + unknown_node.add(json_syntax) + + +def visualize_message(message: ParsedMessage, console: Console = None) -> None: + """Visualize a Claude API message in the terminal.""" + if console is None: + console = Console() + + # Create main tree with token usage in the title + usage_str = "" + if message.usage: + input_tokens = message.usage.get("input_tokens", 0) + output_tokens = message.usage.get("output_tokens", 0) + total_tokens = input_tokens + output_tokens + usage_str = f" [dim white]│[/dim white] [magenta]tokens:[/magenta] [cyan]{input_tokens:,}[/cyan] in • [green]{output_tokens:,}[/green] out • [yellow]{total_tokens:,}[/yellow] total" + + tree = Tree(f"[bold cyan]Claude Message[/bold cyan] ([green]{message.role}[/green]){usage_str}") + + # Add metadata + if message.model: + tree.add(f"[dim white]Model:[/dim white] {message.model}") + + if message.stop_reason: + tree.add(f"[dim white]Stop Reason:[/dim white] {message.stop_reason}") + + # Add content blocks + if message.content: + content_tree = tree.add(f"[bold white]Content[/bold white] ({len(message.content)} blocks)") + for i, content in enumerate(message.content, 1): + block_tree = content_tree.add(f"[dim white]Block {i}[/dim white]") + render_content_block(content, block_tree) + + # Create panel with the tree + panel = Panel( + tree, + title="[bold]Claude API Response[/bold]", + border_style="cyan", + expand=False, + ) + + console.print(panel) + + +class visualize: + """ + Context manager for auto-visualization of Claude API responses. + + Usage: + viz = visualize(auto_show=True) + response = client.messages.create(...) + viz.capture(response) + """ + + def __init__(self, auto_show: bool = True): + """ + Initialize the visualizer. + + Args: + auto_show: Whether to automatically show visualization (default: True) + """ + self.auto_show = auto_show + self.responses = [] + self.console = Console() + + def capture(self, response: Any) -> None: + """ + Capture a response for visualization. + + Args: + response: Claude API response to capture + """ + self.responses.append(response) + + if self.auto_show: + message = parse_response(response) + visualize_message(message, self.console) + + def show_all(self) -> None: + """Show all captured responses.""" + for response in self.responses: + message = parse_response(response) + visualize_message(message, self.console) + + +def show_response(response: Any) -> None: + """ + Simple helper to visualize a single response. + + Args: + response: Claude API response (Message object or dict) + """ + message = parse_response(response) + visualize_message(message) diff --git a/uv.lock b/uv.lock index 5a802060..0d400771 100644 --- a/uv.lock +++ b/uv.lock @@ -127,6 +127,8 @@ dependencies = [ { name = "notebook" }, { name = "numpy" }, { name = "pandas" }, + { name = "python-dotenv" }, + { name = "rich" }, { name = "voyageai" }, ] @@ -147,6 +149,8 @@ requires-dist = [ { name = "notebook", specifier = ">=7.4.7" }, { name = "numpy", specifier = ">=2.3.4" }, { name = "pandas", specifier = ">=2.3.3" }, + { name = "python-dotenv", specifier = ">=1.2.1" }, + { name = "rich", specifier = ">=14.2.0" }, { name = "voyageai", specifier = ">=0.3.5" }, ] @@ -1180,6 +1184,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1222,6 +1238,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "mistune" version = "3.1.4" @@ -1823,6 +1848,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + [[package]] name = "python-json-logger" version = "4.0.0" @@ -1988,6 +2022,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, ] +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + [[package]] name = "rpds-py" version = "0.28.0"