Build Your First AI Agent in Python
Building your first AI agent is surprisingly straightforward: you define available tools (as structured JSON schemas), initialize a language model client, send a message plus tool definitions to the API, and handle the model's tool-call responses in a loop. Within 15 minutes and about 100 lines of Python, you'll have an agent that reasons about which tools to use and executes them to answer real questions.
This tutorial walks you through the complete request/response cycle using the Anthropic Claude API, which has first-class tool-use support. You'll build an agent that accesses a weather tool and a calculator tool, demonstrating both simple tool invocation and looping when the model decides to call multiple tools in sequence.
Prerequisites and Setup
You'll need Python 3.8+, the anthropic library, and a Claude API key. Install the library:
pip install anthropic
Set your API key as an environment variable:
export ANTHROPIC_API_KEY="sk-ant-..."
For this tutorial, we'll simulate tools (the agent calls functions you provide), rather than making real HTTP calls to weather or math APIs. This keeps the example self-contained and runnable immediately.
Defining Tools: The Schema
Before the agent can use tools, it needs a schema describing them. Each tool definition includes a name, description, and input parameters. Here's what a tool schema looks like:
import anthropic
import json
# Define available tools as structured schemas
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a given location",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and state, e.g. San Francisco, CA"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit, default fahrenheit"
}
},
"required": ["location"]
}
},
{
"name": "calculate",
"description": "Perform arithmetic calculations",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Math expression to evaluate, e.g. '2 + 3 * 4'"
}
},
"required": ["expression"]
}
}
]
Each property in the schema tells the model what inputs the tool accepts, their types, and constraints. The model learns from these descriptions and invokes tools intelligently.
Implementing Tool Functions
Next, create Python functions that actually execute when the agent calls a tool. In a real system, these would call APIs or databases:
def execute_tool(tool_name: str, tool_input: dict) -> str:
"""Execute a tool and return the result as a string."""
if tool_name == "get_weather":
location = tool_input.get("location")
unit = tool_input.get("unit", "fahrenheit")
# Simulate weather lookup
if location.lower() == "san francisco":
temp = 65 if unit == "fahrenheit" else 18
return f"Weather in {location}: {temp}°{unit[0].upper()} and clear"
else:
return f"Unable to fetch weather for {location}"
elif tool_name == "calculate":
expression = tool_input.get("expression")
try:
result = eval(expression) # In production, use safer evaluation
return f"Result of '{expression}': {result}"
except Exception as e:
return f"Calculation error: {str(e)}"
else:
return f"Unknown tool: {tool_name}"
Note: eval() is used here for simplicity, but in production use a safe expression evaluator like numexpr or sympy to prevent code injection.
The Agent Loop
The core loop sends the user message to the model, checks if it wants to call tools, executes those tools, and loops back with the results:
def run_agent(user_message: str) -> str:
"""Run an agent that can use tools until it reaches a final answer."""
client = anthropic.Anthropic()
# Initialize conversation with user message
messages = [{"role": "user", "content": user_message}]
# Agent loop: keep going until the model stops calling tools
for iteration in range(10): # Max 10 iterations to prevent infinite loops
print(f"\n--- Iteration {iteration + 1} ---")
# Call Claude with tool definitions
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=messages
)
print(f"Stop reason: {response.stop_reason}")
# Check if the model wants to call tools
if response.stop_reason == "end_turn":
# Model generated a final answer (no tool calls)
for block in response.content:
if hasattr(block, 'text'):
return block.text
# Handle tool calls
if response.stop_reason == "tool_use":
# Add assistant's response to messages
messages.append({"role": "assistant", "content": response.content})
# Execute each tool the model requested
tool_results = []
for block in response.content:
if block.type == "tool_use":
tool_name = block.name
tool_input = block.input
print(f" Calling tool: {tool_name} with input {tool_input}")
# Execute the tool
result = execute_tool(tool_name, tool_input)
print(f" Tool result: {result}")
# Collect tool result for the next message
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
# Send tool results back to the model
messages.append({"role": "user", "content": tool_results})
else:
# Unexpected stop reason
break
return "Max iterations reached"
# Run the agent
if __name__ == "__main__":
result = run_agent("What's the weather in San Francisco and what is 25 * 4?")
print(f"\nFinal answer:\n{result}")
This loop demonstrates the complete flow: user message → model reasoning → tool calls → tool execution → tool results → loop back → final answer.
How the Request/Response Cycle Works
Here's what happens when the agent runs:
- Request: You send the user's message and tool definitions to the Claude API
- Model thinking: Claude receives the tools, your message, and the conversation history. It reasons: "The user wants weather and math. I have tools for both. I'll call them."
- Tool call response: Claude returns
stop_reason: "tool_use"plus a list of tool calls (each specifying tool name and parameters) - Execute locally: Your code executes the tools and collects results
- Tool results back: You send the tool results back to Claude in a new message
- Loop or finish: Claude reasons about the results and either calls more tools or returns a final answer (
stop_reason: "end_turn")
This separation is key: Claude never directly executes code. It suggests tool calls, and your code decides whether to execute them. This is how you maintain control and security.
Running the Agent
Save the code above as agent.py and run it:
python agent.py
Expected output:
--- Iteration 1 ---
Stop reason: tool_use
Calling tool: get_weather with input {'location': 'San Francisco'}
Tool result: Weather in San Francisco: 65°F and clear
Calling tool: calculate with input {'expression': '25 * 4'}
Tool result: Result of '25 * 4': 100
--- Iteration 2 ---
Stop reason: end_turn
Final answer:
The weather in San Francisco is currently 65°F and clear.
And 25 multiplied by 4 equals 100.
The agent called both tools in iteration 1, saw the results in iteration 2, and formulated a final answer.
Key Takeaways
- A tool definition is a JSON schema describing name, description, and input parameters
- The agent loop sends the user message + tools to the model, handles tool calls, executes them, and loops with results
- The model's
stop_reasonindicates whether it wants to call tools ("tool_use") or finish ("end_turn") - Tool execution happens in your code; the model never directly runs code—this is how you maintain security and control
- A working agent requires fewer than 150 lines of Python when using a library like
anthropic
Frequently Asked Questions
What happens if the model calls a tool with invalid parameters?
The API validates parameters against the schema before the model is called, so invalid calls are prevented. If you execute a tool and it fails, you can return an error string as the tool result, and the model will see it and retry or acknowledge the failure.
Can I add more tools dynamically?
Yes. The tools list is just data passed to the API. You can modify it before each call. Some applications build tool lists dynamically based on user context or available plugins.
Does the agent always succeed on first try?
No. If a tool call fails, returns ambiguous data, or the model misinterprets the result, the model can call the tool again with different parameters or try a different tool. This is why agents are more robust than simple scripts for uncertain tasks.
How do I limit the number of tool calls?
Pass a max_tokens limit to the API call. This indirectly limits iterations because the model has fewer tokens to "think" and make multiple decisions. You can also hard-limit iterations in the loop (as done above with range(10)).