Build a Multi-Tool AI Agent with Claude
Build a CLI AI agent that chains weather, math, and timezone tools.
Introduction
30 Second Summary
When you ask a smart assistant about the weather or to do some math, it doesn't just guess. It reaches out to specialized tools behind the scenes, gets real answers, and stitches everything together into a single response.
In this project, you will build your own command-line AI agent powered by Claude that can decide when to call external tools, execute them, and combine the results into natural-language answers.
What You'll Build
A Python-based AI assistant that uses Claude's tool use API to intelligently call a weather checker, a math calculator, and a timezone clock to answer complex, multi-part questions.
By the end of this project, you'll have:
- A working quickstart script that confirms your Claude API setup.
- A full agentic loop that keeps calling tools until Claude has everything it needs to answer.
- Three custom tool definitions with JSON Schema specifications and error handling that lets Claude recover gracefully from failures.
- Secret Mission: add a memory tool that lets Claude store and recall notes across your conversation.
Are there any prerequisites?
You need basic Python knowledge (functions, dictionaries, loops). No prior AI or LLM experience is needed. Step 1 walks you through installing everything from scratch.
Before We Start
Before We Start...
Set Up Python and the Anthropic SDK
Your AI agent will be a Python script that sends messages to Claude and processes the responses.
Before you can write any agent code, you need three things: Python installed on your machine, an Anthropic account with an API key, and the Anthropic SDK that connects your code to Claude.
In this step, get ready to:
- Install Python and verify it works in your terminal.
- Create an Anthropic Console account and set up your API key.
- Install the Anthropic Python SDK.
Install Python
Python is the programming language you'll use to build your agent. The Anthropic SDK requires Python 3.9 or higher.
- Open your browser and go to python.org/downloads.
- Click the big yellow Download Python button to get the latest version for your operating system.
- Run the downloaded installer.
macOS
- Follow the installer prompts and click Continue through each step.
- Click Install when prompted.
The macOS installer automatically adds Python to your PATH.
Windows
- On the very first installer screen, check the box that says Add python.exe to PATH.
- Click Install Now.
Don't skip the PATH checkbox
If you miss the Add python.exe to PATH checkbox, your terminal won't be able to find the python command later. If you already clicked past it, uninstall Python and run the installer again.
Now let's verify the installation works.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open a new terminal inside VS Code by pressing Ctrl+` (the backtick key, above Tab on your keyboard).
- Verify Python is installed by running this command:
python --version
✔️ I see a version number
You should see something like Python 3.14.3 (or any version 3.9 or higher). Python is ready to go.
ⓧ Command not found
If you see an error like 'python' is not recognized or command not found, Python isn't on your PATH.
- On Windows: uninstall Python, re-run the installer, and this time check Add python.exe to PATH before clicking Install.
- On macOS: try running python3 --version instead. If that works, use python3 and pip3 for all commands in this project.
- Close your terminal and open a new one. Environment changes from installation only take effect in new terminal sessions.
Create your Anthropic account and API key
To call Claude from your code, you need an API key from Anthropic's developer console. New accounts get $5 in free credits, which is more than enough for this project.
- Open console.anthropic.com in your browser.
- Sign up for a new account (or sign in if you already have one).
- Click Settings in the left sidebar.
- Click API Keys.
- Click Create Key and give it a name like ai-agent-project.
- Copy the key immediately. You won't be able to see it again after closing the dialog.
What is an API key?
An API key is a secret password that identifies your code to Anthropic's servers. When your Python script sends a request to Claude, it includes this key so Anthropic knows it's you (and can charge your credits accordingly).
Now you need to store the key as an environment variable so the SDK can find it automatically. The Anthropic SDK looks for a variable named ANTHROPIC_API_KEY.
- In your VS Code terminal, set the environment variable by running this command (replace your-api-key-here with the key you just copied):
macOS
echo 'export ANTHROPIC_API_KEY="your-api-key-here"' >> ~/.zshrc
source ~/.zshrc
What does this command do?
The first line saves the variable to your shell config file so it persists across terminal sessions. The second line reloads that config file so the variable is available right now.
Windows
setx ANTHROPIC_API_KEY "your-api-key-here"
What does this command do?
The setx command saves the variable permanently to your Windows environment. It will be available in any new terminal session you open after this.
- Close your current terminal by clicking the trash icon in the terminal panel.
- Open a fresh terminal by pressing Ctrl+` again.
You must reopen the terminal
On Windows, setx only takes effect in new terminal sessions. If you skip this step, the SDK won't find your key and your code will fail with an authentication error.
Create your project folder and install the SDK
You need a dedicated folder for your agent code. You'll create one on your Desktop so it's easy to find.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir claude-agent
cd claude-agent
- Open this folder in VS Code by running:
code .
What does code . do?
The code command launches VS Code from the terminal. The . tells it to open the current folder. A new VS Code window will appear with your claude-agent folder loaded in the sidebar.
- If a new VS Code window opens, open a terminal in it by pressing Ctrl+`.
- Install the Anthropic Python SDK by running:
pip install anthropic
You should see pip downloading and installing the package along with its dependencies.
- Confirm the SDK installed correctly by running:
python -c "import anthropic; print(anthropic.__version__)"
You should see a version number printed, like 0.99.0 or similar.
Seeing a ModuleNotFoundError?
If you get ModuleNotFoundError: No module named 'anthropic', your pip and python commands might be pointing to different Python installations.
- Try running pip3 install anthropic instead, then verify with python3 -c "import anthropic; print(anthropic.__version__)".
- Make sure you're in the same terminal session where Python is recognized.
Still stuck? Help me fix this ModuleNotFoundError for the anthropic package.
Your development environment is fully set up. Next, you'll make your first API call to Claude and see a real response come back.
Make Your First Claude API Call
Your Python environment is configured and the Anthropic SDK is installed. Now let's make sure everything actually works by talking to Claude directly.
You'll create a short script that sends a message to the Claude API and prints the response. If your API key, SDK, or network connection has any issues, this is where you'll catch them.
In this step, get ready to:
- Create quickstart.py and make a basic API call to Claude.
- Examine the response structure to understand how Claude returns data.
Create quickstart.py and call the API
The Anthropic SDK provides a messages.create method that sends a conversation to Claude and returns a structured response. You'll use this method for every interaction with Claude throughout the project.
- Create a new file called quickstart.py in your project folder by right-clicking in the VS Code Explorer sidebar and selecting New File.
- Paste the following code into quickstart.py:
import anthropic
# Initialize the client - reads ANTHROPIC_API_KEY from your environment automatically
client = anthropic.Anthropic()
# Send a message to Claude
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[
{
"role": "user",
"content": "What is tool use in AI and why is it useful? Answer in 2-3 sentences.",
}
],
)
# Print just the text from Claude's response
print(message.content[0].text)
What does this code do?
- anthropic.Anthropic() creates a client that automatically reads your ANTHROPIC_API_KEY environment variable. No need to pass the key manually.
- messages.create() sends your conversation to Claude. It requires three parameters: model (which version of Claude to use), max_tokens (the response length limit), and messages (the conversation so far).
- message.content[0].text extracts Claude's text response from the structured response object.
- Save quickstart.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Run the script by entering this command in your VS Code terminal:
python quickstart.py
You should see a 2-3 sentence explanation of tool use in AI printed in your terminal. The exact wording will vary each time you run it.
Seeing an error instead of a response?
- AuthenticationError: Your API key isn't being read. Make sure you closed and reopened your terminal after running setx in Step 1. Check the key is set by running echo %ANTHROPIC_API_KEY% in Command Prompt.
- ModuleNotFoundError: No module named 'anthropic': The SDK isn't installed in the Python that's running your script. Run pip install anthropic again and make sure there are no errors.
- NotFoundError or invalid model: Double-check that the model string is exactly claude-sonnet-4-20250514 with no typos.
Help me debug my quickstart.py error
Examine the response structure
Claude's response isn't a plain string. It's a structured object with several fields that become important once you start using tools.
How is Claude's response structured?
The message object returned by messages.create() has two key fields you'll use throughout this project:
- message.content is a list of content blocks. Right now it contains a single TextBlock with Claude's text answer. Later, when you add tools, this list will also contain ToolUseBlock objects.
- message.stop_reason tells you why Claude stopped generating. For this response, it's "end_turn", meaning Claude finished its answer naturally. When Claude wants to use a tool, this changes to "tool_use". That distinction is the foundation of your entire agent.
Why does content come back as a list?
Claude can return multiple content blocks in a single response. For example, it might return a TextBlock saying "Let me check the weather" followed by a ToolUseBlock requesting the weather tool. The list structure handles both simple responses and complex multi-step ones.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's the complete quickstart.py file for comparison:
import anthropic
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[
{
"role": "user",
"content": "What is tool use in AI and why is it useful? Answer in 2-3 sentences.",
}
],
)
print(message.content[0].text)
Your API connection is confirmed and you understand how Claude structures its responses. Next up, you'll define your first tool and teach Claude how to use it.
Define Your First Tool and Handle the Lifecycle
You confirmed your API connection works in the last step. Now it's time to give Claude something much more powerful: the ability to call external tools.
Tool use lets Claude reach beyond text generation. Instead of guessing the weather, Claude can call a function you define and use real data in its answer. In this step, you'll define your first tool and walk through the complete lifecycle of a tool call.
In this step, get ready to:
- Define a weather tool with a JSON Schema specification.
- Implement a function that executes the tool when Claude requests it.
- Observe the full tool_use lifecycle from start to finish.
Create agent.py with your first tool definition
Tool definitions tell Claude what tools are available, what they do, and what parameters they accept. Each tool is a dictionary with three fields: a name, a description, and an input_schema written in JSON Schema format.
- In VS Code, create a new file called agent.py in your project folder (the same folder where quickstart.py lives).
- Add the following imports, client setup, and tool definition by pasting this code:
import json
from datetime import datetime
from zoneinfo import ZoneInfo
import anthropic
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-20250514"
# Define tools with JSON Schema specifications
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a given city. Returns temperature, condition, and humidity. Use this when the user asks about weather in a specific location.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'Tokyo' or 'New York'",
}
},
"required": ["city"],
},
},
]
What does this code do?
- client = anthropic.Anthropic() initializes the SDK and automatically reads your ANTHROPIC_API_KEY environment variable.
- MODEL stores the model ID as a constant so you only need to change it in one place.
- tools is a list of tool definitions that gets passed to the API alongside your messages. Claude reads these to know what it can call.
- input_schema uses JSON Schema to tell Claude exactly what parameters the tool accepts. Here, city is a required string.
Why does the description matter so much?
Claude uses the description field to decide WHEN to use a tool. A vague description means Claude might call the tool at the wrong time or skip it entirely. Write descriptions that explain what the tool returns and when it should be used.
- Save agent.py.
Implement the run_tool function
When Claude decides to use a tool, it doesn't execute anything itself. It returns a tool_use block containing the tool name and inputs. Your code is responsible for actually running the tool and sending back the result.
- In agent.py, add the run_tool function below the tools list by pasting this code:
def run_tool(name, tool_input):
"""Execute a tool and return the result as a JSON string."""
if name == "get_weather":
city = tool_input["city"]
# Simulated weather data - in a real app, this would call a weather API
weather_data = {
"city": city,
"temperature_celsius": 22,
"condition": "Partly cloudy",
"humidity_percent": 65,
}
return json.dumps(weather_data)
else:
raise ValueError(f"Unknown tool: {name}")
What does this code do?
- run_tool takes the tool name and its input (both provided by Claude) and executes the matching logic.
- The weather data is simulated here. In a production app, this would call a real weather API. The pattern is identical either way.
- json.dumps() converts the Python dictionary into a JSON string that Claude can read.
- The else: raise ValueError catches any tool name that doesn't match, which protects against unexpected calls.
- Save agent.py.
Seeing a syntax error?
Make sure the run_tool function is placed AFTER the closing ] of the tools list. There should be a blank line between the end of the list and the function definition.
help me fix a syntax error in my agent.py file
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's the full agent.py so far:
import json
from datetime import datetime
from zoneinfo import ZoneInfo
import anthropic
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-20250514"
# Define tools with JSON Schema specifications
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a given city. Returns temperature, condition, and humidity. Use this when the user asks about weather in a specific location.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'Tokyo' or 'New York'",
}
},
"required": ["city"],
},
},
]
def run_tool(name, tool_input):
"""Execute a tool and return the result as a JSON string."""
if name == "get_weather":
city = tool_input["city"]
# Simulated weather data - in a real app, this would call a weather API
weather_data = {
"city": city,
"temperature_celsius": 22,
"condition": "Partly cloudy",
"humidity_percent": 65,
}
return json.dumps(weather_data)
else:
raise ValueError(f"Unknown tool: {name}")
Test the full tool_use lifecycle
Now comes the exciting part. You'll send Claude a question that triggers the weather tool, then manually walk through each step of the lifecycle to see exactly how it works.
What is the tool_use lifecycle?
The lifecycle has four stages:
- You send a user message plus the tools array to Claude.
- Claude returns stop_reason: "tool_use" with a tool_use block containing the tool name, inputs, and a unique id.
- You execute the tool with those inputs and send back a tool_result message with the matching tool_use_id.
- Claude reads the result and produces a final natural-language answer.
- Open your terminal in VS Code and run this test script to see the lifecycle in action:
python -c "
import json
import anthropic
client = anthropic.Anthropic()
MODEL = 'claude-sonnet-4-20250514'
tools = [
{
'name': 'get_weather',
'description': 'Get the current weather for a given city. Returns temperature, condition, and humidity. Use this when the user asks about weather in a specific location.',
'input_schema': {
'type': 'object',
'properties': {
'city': {
'type': 'string',
'description': \"The city name, e.g. 'Tokyo' or 'New York'\",
}
},
'required': ['city'],
},
},
]
# Step 1: Send a message with tools
print('--- Step 1: Sending message with tools ---')
response = client.messages.create(
model=MODEL,
max_tokens=1024,
tools=tools,
messages=[{'role': 'user', 'content': \"What's the weather like in Tokyo?\"}],
)
print(f'Stop reason: {response.stop_reason}')
# Step 2: Extract the tool_use block
tool_use_block = next(block for block in response.content if block.type == 'tool_use')
print(f'\n--- Step 2: Claude wants to call a tool ---')
print(f'Tool name: {tool_use_block.name}')
print(f'Tool input: {tool_use_block.input}')
print(f'Tool use ID: {tool_use_block.id}')
# Step 3: Execute the tool and return the result
weather_data = {'city': 'Tokyo', 'temperature_celsius': 22, 'condition': 'Partly cloudy', 'humidity_percent': 65}
result = json.dumps(weather_data)
print(f'\n--- Step 3: Executing tool, returning result ---')
print(f'Tool result: {result}')
# Step 4: Send tool_result back to Claude
final_response = client.messages.create(
model=MODEL,
max_tokens=1024,
tools=tools,
messages=[
{'role': 'user', 'content': \"What's the weather like in Tokyo?\"},
{'role': 'assistant', 'content': response.content},
{'role': 'user', 'content': [{'type': 'tool_result', 'tool_use_id': tool_use_block.id, 'content': result}]},
],
)
print(f'\n--- Step 4: Claude\'s final answer ---')
print(final_response.content[0].text)
"
You should see output that walks through all four stages of the lifecycle. Claude's stop reason is tool_use (not end_turn), it names the get_weather tool with Tokyo as the city, and then produces a friendly summary of the weather after receiving the result.
What is tool_use_id and why does it matter?
Every tool_use block has a unique id field. When you return the result, you MUST include this exact ID in your tool_result message. This is how Claude matches a result back to the specific tool call it made.
This becomes critical when Claude makes multiple tool calls at once. Each result needs the correct ID so Claude knows which answer goes with which question.
Getting an authentication or connection error?
If you see AuthenticationError, your API key isn't being read. Make sure you reopened your terminal after running setx in the previous step. You can verify the key exists by running:
echo %ANTHROPIC_API_KEY%
If this prints your key, the terminal can see it. If it prints nothing, close and reopen VS Code entirely.
help me fix an authentication error with the Anthropic API
You just completed a full round trip with Claude's tool use API. Your agent.py file has the tool definition and executor ready to go. Next up, you'll add more tools and build the agentic loop that handles everything automatically.
Add Multiple Tools and Build the Agentic Loop
Your agent currently has one tool and handles a single tool call before stopping. Real-world questions often require multiple tools working together.
In this step, you will expand your agent's toolbox and build the agentic loop. This is the core pattern that keeps your agent running until Claude has all the information it needs to answer.
In this step, get ready to:
- Add a math calculator and timezone clock to your agent's toolbox.
- Build an agentic loop that processes tool calls until Claude finishes.
- Add error handling so Claude can recover gracefully when tools fail.
Add the calculate and get_time tools
Claude decides which tools to use based on their schemas. Each schema describes what a tool does, what parameters it accepts, and when to use it.
- In agent.py, find the closing ] bracket of the tools list (after the get_weather definition).
- Add these two new tool definitions inside the list, after the get_weather dictionary:
{
"name": "calculate",
"description": "Evaluate a mathematical expression and return the numeric result. Supports addition, subtraction, multiplication, division, and exponents. Use this for any math the user needs computed precisely.",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "The math expression to evaluate, e.g. '(28 * 9/5) + 32' or '2 ** 10'",
}
},
"required": ["expression"],
},
},
{
"name": "get_time",
"description": "Get the current time in a specific timezone. Use IANA timezone names like 'Asia/Tokyo', 'America/New_York', 'Europe/London'. Use this when the user asks what time it is somewhere.",
"input_schema": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "IANA timezone name, e.g. 'Asia/Tokyo', 'America/New_York', 'Europe/London'",
}
},
"required": ["timezone"],
},
},
What do these tool schemas do?
Each schema has three key fields that Claude reads to decide when and how to use a tool:
- name is the identifier Claude uses to request the tool.
- description tells Claude when to pick this tool over others.
- input_schema defines what parameters the tool needs, using JSON Schema format.
Now you need to implement the logic that runs when Claude calls these tools.
- In the run_tool function, find the else: block at the bottom.
- Replace that else: block with these two new elif cases, keeping the else at the very end:
elif name == "calculate":
expression = tool_input["expression"]
# Only allow safe math characters
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
raise ValueError(f"Invalid characters in expression: {expression}")
result = eval(expression)
return json.dumps({"expression": expression, "result": result})
elif name == "get_time":
timezone = tool_input["timezone"]
try:
tz = ZoneInfo(timezone)
current_time = datetime.now(tz)
return json.dumps(
{
"timezone": timezone,
"time": current_time.strftime("%I:%M %p"),
"date": current_time.strftime("%A, %B %d, %Y"),
}
)
except KeyError:
raise ValueError(f"Unknown timezone: {timezone}")
else:
raise ValueError(f"Unknown tool: {name}")
What does this code do?
- calculate validates the expression against a set of safe characters before using eval(). This prevents code injection by only allowing digits and math operators.
- get_time uses Python's built-in ZoneInfo class to get real, live time data for any IANA timezone.
- Both tools return JSON strings. Claude reads these results to compose its final natural-language answer.
- Save agent.py.
Seeing a syntax error?
- Make sure the two new tool definitions are separated by commas inside the tools list. Each dictionary needs a trailing comma.
- Check that the elif blocks are at the same indentation level as the original if name == "get_weather": line.
- The else: raise ValueError(...) must remain as the very last branch in run_tool.
Why is my run_tool function giving me an indentation or syntax error?
Build the agentic loop
Right now, your agent handles exactly one tool call and stops. Complex questions need multiple tool calls in sequence.
The agentic loop keeps processing tool calls in a while loop until Claude's stop_reason is no longer "tool_use". This is the same pattern used in production AI assistants.
- Add the following chat() function at the bottom of agent.py, below the run_tool function:
def chat(user_message, conversation_history):
"""Send a message and handle the full agentic tool loop."""
conversation_history.append({"role": "user", "content": user_message})
response = client.messages.create(
model=MODEL,
max_tokens=1024,
tools=tools,
messages=conversation_history,
)
# Agentic loop: keep processing tool calls until Claude is done
while response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f" [Calling tool: {block.name}({json.dumps(block.input)})]")
try:
result = run_tool(block.name, block.input)
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
}
)
except Exception as e:
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(e),
"is_error": True,
}
)
# Add assistant response and tool results to conversation history
conversation_history.append({"role": "assistant", "content": response.content})
conversation_history.append({"role": "user", "content": tool_results})
# Continue the conversation
response = client.messages.create(
model=MODEL,
max_tokens=1024,
tools=tools,
messages=conversation_history,
)
# Extract the final text response
conversation_history.append({"role": "assistant", "content": response.content})
final_text = next(
(block.text for block in response.content if hasattr(block, "text")),
"No response generated.",
)
return final_text
What does the agentic loop do?
This function drives the entire tool use lifecycle. Here is how each part works:
- while response.stop_reason == "tool_use" keeps the loop running as long as Claude wants to call more tools.
- The for block in response.content loop handles parallel tool calls. Claude can request multiple tools in a single response.
- Each tool_result must include the matching tool_use_id so Claude knows which result goes with which request.
- Both the assistant response and tool results get appended to conversation_history so Claude has full context on the next API call.
- When Claude is done calling tools, stop_reason changes to "end_turn" and the loop exits.
- Save agent.py.
- You won't see a change yet. The next step builds the interactive interface that calls this function.
Unsure about the indentation?
- The def chat(...) line must be at the top level (no indentation), the same level as def run_tool(...).
- The while loop is indented one level inside the function. The for loop is indented another level inside the while.
- The try/except block is indented inside the if block.type == "tool_use": check.
Can you help me fix the indentation in my chat function?
Handle tool errors gracefully
Tools can fail. A timezone might be misspelled, or a math expression might contain invalid characters. Without error handling, one bad tool call would crash your entire agent.
The chat() function you just wrote already includes the error handling pattern. Take a look at the try/except block inside the loop:
try:
result = run_tool(block.name, block.input)
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
}
)
except Exception as e:
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(e),
"is_error": True,
}
)
How does is_error work?
When a tool fails, you still send a tool_result back to Claude. The is_error: True flag tells Claude the result is an error message, not real data.
Claude reads the error and decides what to do next. It might retry with different input, try a different tool, or explain to the user what went wrong. The agent never crashes.
Why not just crash on errors?
In a single-turn script, crashing is acceptable. In an agentic loop, crashing wastes all the work the agent already did. The is_error pattern keeps the conversation alive and lets Claude self-correct.
✔️ Awesome, I've got everything!
Double check that you have saved agent.py with all three tool definitions, three run_tool handlers, and the complete chat() function.
ⓧ I'd like to double check the full code
Here is the full agent.py as it should look at the end of this step:
import json
from datetime import datetime
from zoneinfo import ZoneInfo
import anthropic
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-20250514"
# Define tools with JSON Schema specifications
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a given city. Returns temperature, condition, and humidity. Use this when the user asks about weather in a specific location.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'Tokyo' or 'New York'",
}
},
"required": ["city"],
},
},
{
"name": "calculate",
"description": "Evaluate a mathematical expression and return the numeric result. Supports addition, subtraction, multiplication, division, and exponents. Use this for any math the user needs computed precisely.",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "The math expression to evaluate, e.g. '(28 * 9/5) + 32' or '2 ** 10'",
}
},
"required": ["expression"],
},
},
{
"name": "get_time",
"description": "Get the current time in a specific timezone. Use IANA timezone names like 'Asia/Tokyo', 'America/New_York', 'Europe/London'. Use this when the user asks what time it is somewhere.",
"input_schema": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "IANA timezone name, e.g. 'Asia/Tokyo', 'America/New_York', 'Europe/London'",
}
},
"required": ["timezone"],
},
},
]
def run_tool(name, tool_input):
"""Execute a tool and return the result as a JSON string."""
if name == "get_weather":
city = tool_input["city"]
weather_data = {
"city": city,
"temperature_celsius": 22,
"condition": "Partly cloudy",
"humidity_percent": 65,
}
return json.dumps(weather_data)
elif name == "calculate":
expression = tool_input["expression"]
# Only allow safe math characters
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
raise ValueError(f"Invalid characters in expression: {expression}")
result = eval(expression)
return json.dumps({"expression": expression, "result": result})
elif name == "get_time":
timezone = tool_input["timezone"]
try:
tz = ZoneInfo(timezone)
current_time = datetime.now(tz)
return json.dumps(
{
"timezone": timezone,
"time": current_time.strftime("%I:%M %p"),
"date": current_time.strftime("%A, %B %d, %Y"),
}
)
except KeyError:
raise ValueError(f"Unknown timezone: {timezone}")
else:
raise ValueError(f"Unknown tool: {name}")
def chat(user_message, conversation_history):
"""Send a message and handle the full agentic tool loop."""
conversation_history.append({"role": "user", "content": user_message})
response = client.messages.create(
model=MODEL,
max_tokens=1024,
tools=tools,
messages=conversation_history,
)
# Agentic loop: keep processing tool calls until Claude is done
while response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f" [Calling tool: {block.name}({json.dumps(block.input)})]")
try:
result = run_tool(block.name, block.input)
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
}
)
except Exception as e:
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(e),
"is_error": True,
}
)
conversation_history.append({"role": "assistant", "content": response.content})
conversation_history.append({"role": "user", "content": tool_results})
response = client.messages.create(
model=MODEL,
max_tokens=1024,
tools=tools,
messages=conversation_history,
)
conversation_history.append({"role": "assistant", "content": response.content})
final_text = next(
(block.text for block in response.content if hasattr(block, "text")),
"No response generated.",
)
return final_text
Your agent now has three tools and an agentic loop that can chain them together. Next up, you will build the interactive chat interface that puts this all into action.
Build the Interactive Chat Interface
Your agent can now handle multiple tools, chain them together, and recover from errors. The missing piece is a way to actually talk to it.
Right now, every test requires editing the script and re-running it. In this step, you will wrap your agent in an interactive chat loop so you can have a back-and-forth conversation with Claude directly from your terminal.
In this step, get ready to:
- Add a main() function with a welcome banner and interactive input loop.
- Test your agent with multi-tool queries that chain tool calls together.
- Observe how Claude decides when to use tools and when to answer directly.
Add the interactive main() function
The chat() function you built handles a single turn of conversation. A main() function will wrap it in a loop that keeps accepting input until you type quit.
- In agent.py, scroll to the very bottom of the file (after the chat() function) and add the following code:
def main():
print("=" * 50)
print(" Multi-Tool AI Agent powered by Claude")
print("=" * 50)
print("\nI have access to these tools:")
print(" - get_weather: Check weather in any city")
print(" - calculate: Evaluate math expressions")
print(" - get_time: Get current time in any timezone")
print("\nTry asking complex questions that need multiple tools!")
print("Type 'quit' to exit.\n")
# Keeps the full conversation so Claude remembers context
conversation_history = []
while True:
user_input = input("You: ").strip()
if user_input.lower() in ("quit", "exit", "q"):
print("Goodbye!")
break
if not user_input:
continue
response = chat(user_input, conversation_history)
print(f"\nClaude: {response}\n")
if __name__ == "__main__":
main()
What does this code do?
- The print() lines at the top create a welcome banner that tells you which tools are available.
- conversation_history = [] creates an empty list that grows with every message. This is how Claude remembers what you said earlier in the conversation.
- The while True loop keeps prompting you for input. Typing quit, exit, or q breaks out of the loop and ends the program.
- if __name__ == "__main__" ensures main() only runs when you execute the file directly, not when importing it as a module.
- Save agent.py.
Seeing an indentation error?
Make sure def main(): starts at the very left edge of the file (no spaces before it). It should be at the same indentation level as def chat() and def run_tool() above it.
help me fix an indentation error in my agent.py file
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete agent.py file. Compare yours against it:
import json
from datetime import datetime
from zoneinfo import ZoneInfo
import anthropic
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-20250514"
# Define tools with JSON Schema specifications
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a given city. Returns temperature, condition, and humidity. Use this when the user asks about weather in a specific location.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'Tokyo' or 'New York'",
}
},
"required": ["city"],
},
},
{
"name": "calculate",
"description": "Evaluate a mathematical expression and return the numeric result. Supports addition, subtraction, multiplication, division, and exponents. Use this for any math the user needs computed precisely.",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "The math expression to evaluate, e.g. '(28 * 9/5) + 32' or '2 ** 10'",
}
},
"required": ["expression"],
},
},
{
"name": "get_time",
"description": "Get the current time in a specific timezone. Use IANA timezone names like 'Asia/Tokyo', 'America/New_York', 'Europe/London'. Use this when the user asks what time it is somewhere.",
"input_schema": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "IANA timezone name, e.g. 'Asia/Tokyo', 'America/New_York', 'Europe/London'",
}
},
"required": ["timezone"],
},
},
]
def run_tool(name, tool_input):
"""Execute a tool and return the result as a JSON string."""
if name == "get_weather":
city = tool_input["city"]
# Simulated weather data - in a real app, this would call a weather API
weather_data = {
"city": city,
"temperature_celsius": 22,
"condition": "Partly cloudy",
"humidity_percent": 65,
}
return json.dumps(weather_data)
elif name == "calculate":
expression = tool_input["expression"]
# Only allow safe math characters
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
raise ValueError(f"Invalid characters in expression: {expression}")
result = eval(expression)
return json.dumps({"expression": expression, "result": result})
elif name == "get_time":
timezone = tool_input["timezone"]
try:
tz = ZoneInfo(timezone)
current_time = datetime.now(tz)
return json.dumps(
{
"timezone": timezone,
"time": current_time.strftime("%I:%M %p"),
"date": current_time.strftime("%A, %B %d, %Y"),
}
)
except KeyError:
raise ValueError(f"Unknown timezone: {timezone}")
else:
raise ValueError(f"Unknown tool: {name}")
def chat(user_message, conversation_history):
"""Send a message and handle the full agentic tool loop."""
conversation_history.append({"role": "user", "content": user_message})
response = client.messages.create(
model=MODEL,
max_tokens=1024,
tools=tools,
messages=conversation_history,
)
# Agentic loop: keep processing tool calls until Claude is done
while response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f" [Calling tool: {block.name}({json.dumps(block.input)})]")
try:
result = run_tool(block.name, block.input)
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
}
)
except Exception as e:
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(e),
"is_error": True,
}
)
# Add assistant response and tool results to conversation history
conversation_history.append({"role": "assistant", "content": response.content})
conversation_history.append({"role": "user", "content": tool_results})
# Continue the conversation
response = client.messages.create(
model=MODEL,
max_tokens=1024,
tools=tools,
messages=conversation_history,
)
# Extract the final text response
conversation_history.append({"role": "assistant", "content": response.content})
final_text = next(
(block.text for block in response.content if hasattr(block, "text")),
"No response generated.",
)
return final_text
def main():
print("=" * 50)
print(" Multi-Tool AI Agent powered by Claude")
print("=" * 50)
print("\nI have access to these tools:")
print(" - get_weather: Check weather in any city")
print(" - calculate: Evaluate math expressions")
print(" - get_time: Get current time in any timezone")
print("\nTry asking complex questions that need multiple tools!")
print("Type 'quit' to exit.\n")
conversation_history = []
while True:
user_input = input("You: ").strip()
if user_input.lower() in ("quit", "exit", "q"):
print("Goodbye!")
break
if not user_input:
continue
response = chat(user_input, conversation_history)
print(f"\nClaude: {response}\n")
if __name__ == "__main__":
main()
Test with multi-tool queries
The real test of an agentic loop is asking questions that require multiple tool calls. Claude should chain tools together automatically without any extra prompting from you.
- Run your agent by executing this command in the terminal:
python agent.py
You should see the welcome banner listing the three available tools and a You: prompt waiting for your input.
- Try this multi-tool query that requires both the timezone tool and math:
What time is it in London and Tokyo right now?
You should see two [Calling tool: get_time(...)] log lines appear before Claude composes its answer. This confirms the agentic loop is processing parallel tool calls.
- Now try a query that chains different tools together:
If it's 28 degrees Celsius in Paris, convert that to Fahrenheit.
Claude should call calculate with the Fahrenheit conversion formula. You will see the tool call log showing the math expression Claude chose to evaluate.
Agent crashes or shows an error?
If you see AuthenticationError, your API key is not set in this terminal session. Close the terminal, reopen it, and try again.
If you see RateLimitError, wait 30 seconds and try again. Free-tier accounts have lower rate limits.
help me debug my agent.py error
Observe Claude's decision-making
Claude does not blindly call tools on every message. It evaluates whether a tool is needed and answers directly when it already knows the answer.
- Try asking something Claude can answer without tools:
What is the capital of France?
Notice that no [Calling tool: ...] log appears. Claude responded directly because none of the available tools were relevant to that question.
How does Claude choose when to use tools?
Claude reads the description field in each tool definition to decide relevance. If your question matches what a tool can do, Claude calls it. If not, Claude answers from its own knowledge.
This is why writing clear, specific descriptions matters. A vague description leads to Claude calling tools when it should not (or missing opportunities to call them when it should).
- Try one more query that tests conversation memory:
What's the weather in Tokyo?
- After Claude responds, follow up with:
What about New York?
Claude remembers you were asking about weather and calls get_weather for New York without you needing to repeat the full question. This works because conversation_history preserves the full context across turns.
- Type quit to exit the agent when you are done testing.
Your multi-tool AI agent is fully operational. You have the exact same architecture that powers production AI assistants: tool definitions, an agentic loop, conversation memory, and graceful error handling. Next up, the Secret Mission gives you a chance to extend your agent with a brand new capability.
Secret mission
Add a Memory Tool
Your agent can check the weather, do math, and tell time. But it forgets everything the moment you move on to a new question. What if Claude could remember things you tell it and recall them later in the conversation?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and only uses API credits when you actively run your scripts. There are no ongoing costs while your code is sitting idle.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Python installation on your computer.
- Anthropic Console account and API key.
- Project folder with quickstart.py and agent.py.
- Anthropic Python SDK (installed via pip).
- ANTHROPIC_API_KEY environment variable.
Keep everything running
No action needed. Choose this if you plan to keep experimenting with your agent or want to add more tools.
- Your project files stay on your Desktop.
- Your API key stays active. You still have remaining free credits that are valid for 30 days from account creation.
- No charges occur unless you run a script that calls the API.
Pause - I'll come back to this later
Remove your API key from the environment to prevent accidental usage. Keep your project files for when you return.
- Remove the environment variable by opening a terminal and running:
setx ANTHROPIC_API_KEY ""
- Close and reopen your terminal for the change to take effect.
- Keep your project folder on your Desktop for when you want to continue.
- When you return, set the environment variable again with your API key.
Delete - I don't want to use this again
Remove all project files, the environment variable, and optionally your API key from the Anthropic Console.
- Delete your project folder from the Desktop.
- Remove the environment variable by running:
setx ANTHROPIC_API_KEY ""
- Close and reopen your terminal for the change to take effect.
- Go to console.anthropic.com, navigate to Settings then API Keys, and delete the key you created.
- Uninstall the SDK by running:
pip uninstall anthropic
Nice Work!
Nice Work!
You just built a fully functional AI agent from scratch. Your agent can understand natural language questions, decide which tools to use, and chain multiple tool calls together to compose complete answers.
Here's what you learned:
- Built a working multi-tool AI agent powered by Claude's tool use API, complete with JSON Schema tool definitions for weather, math, and timezone lookups.
- Mastered the tool_use lifecycle: defining schemas, handling tool_use blocks, returning tool_result responses with matching tool_use_id values, and looping until Claude is done.
- Implemented the agentic loop pattern that powers production AI assistants, with parallel tool call support and graceful error recovery using is_error.
- Added a memory system that lets Claude store and recall information across turns, turning your agent into a persistent assistant.
Ready to quiz yourself?