What You'll Build
By the end of this tutorial, you'll have a working autonomous AI agent in Python that can reason through multi-step problems, call real tools, and loop until it has a final answer. It uses the Claude API with tool use — so the agent actually decides when and how to use each function, not you. This is the same pattern we use at Naples AI when building intelligent automation systems for local businesses.
Prerequisites
- Python 3.9 or higher installed
- An Anthropic API key (get one here)
- Basic familiarity with Python functions and classes
anthropicSDK installed:pip install anthropic- A terminal or code editor (VS Code works great)
The complete working agent code is built step by step below. By the end of Step 4, you'll have everything you need to copy, run, and extend. Each snippet builds on the last, so I'd recommend reading through once before running anything.
Step 1: Set Up Your Claude API Client
First, let's install the SDK and wire up the client. This part is quick — the Anthropic SDK handles authentication with a single environment variable.
Set your API key in your terminal before running anything:
terminalexport ANTHROPIC_API_KEY="your-api-key-here"
Now create your project file and set up the client:
agent.pyimport anthropic
import os
# Initialize the client — it automatically reads ANTHROPIC_API_KEY from env
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
# Quick test to confirm your connection works
def test_connection():
response = client.messages.create(
model=MODEL,
max_tokens=64,
messages=[{"role": "user", "content": "Say: connection successful"}]
)
print(response.content[0].text)
test_connection()Run this and you should see connection successful in your terminal. If you get an authentication error, double-check your API key is exported correctly in the same shell session.
Step 2: Define Agent Tools and Functions
This is where agents get interesting. Tools are functions you define in Python, but the agent decides when to call them. You give Claude a schema that describes what each tool does and what parameters it takes.
We're building an agent that can search a knowledge base, do math calculations, and look up weather data. These three tools cover the core patterns you'll see in almost every real-world agent.
agent.pyimport anthropic
import os
import json
import math
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
# Tool schemas tell Claude what tools exist and how to call them.
# The "input_schema" follows JSON Schema format.
TOOLS = [
{
"name": "calculate",
"description": "Perform mathematical calculations. Use this for any arithmetic, percentages, or numeric operations.",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "A valid Python math expression, e.g. '(150 * 0.08) + 200'"
}
},
"required": ["expression"]
}
},
{
"name": "search_knowledge_base",
"description": "Search a local knowledge base for information about Naples AI services and pricing.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query to look up in the knowledge base"
}
},
"required": ["query"]
}
},
{
"name": "get_weather",
"description": "Get the current weather for a city. Returns temperature and conditions.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'Naples, FL'"
}
},
"required": ["city"]
}
}
]Write your tool descriptions like you're explaining them to a smart intern who has never seen your codebase. The clearer the description, the better Claude decides when (and when not) to use each tool. Vague descriptions lead to the agent calling the wrong tool or hallucinating parameters.
Step 3: Build the Agent Decision Loop
This is the heart of any autonomous agent. The loop works like this: send a message to Claude, check if it wants to use a tool, execute that tool, feed the result back, and repeat until Claude gives a final text answer. The agent keeps going until it's done — you don't have to manage the flow manually.
agent.pyimport anthropic
import os
import json
import math
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
TOOLS = [
{
"name": "calculate",
"description": "Perform mathematical calculations. Use this for any arithmetic, percentages, or numeric operations.",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "A valid Python math expression, e.g. '(150 * 0.08) + 200'"
}
},
"required": ["expression"]
}
},
{
"name": "search_knowledge_base",
"description": "Search a local knowledge base for information about Naples AI services and pricing.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query to look up in the knowledge base"
}
},
"required": ["query"]
}
},
{
"name": "get_weather",
"description": "Get the current weather for a city. Returns temperature and conditions.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'Naples, FL'"
}
},
"required": ["city"]
}
}
]
class AIAgent:
def __init__(self, system_prompt: str):
self.client = anthropic.Anthropic()
self.model = MODEL
self.tools = TOOLS
self.system_prompt = system_prompt
self.messages = [] # Stores the full conversation history
def run(self, user_message: str) -> str:
"""
Main agent loop. Sends user message, handles tool calls,
and returns the final text response.
"""
print(f"\n👤 User: {user_message}")
self.messages.append({"role": "user", "content": user_message})
# Keep looping until Claude returns a final answer (no more tool calls)
while True:
response = self.client.messages.create(
model=self.model,
max_tokens=4096,
system=self.system_prompt,
tools=self.tools,
messages=self.messages
)
# Add Claude's response to message history
self.messages.append({"role": "assistant", "content": response.content})
# If Claude is done reasoning and has a final answer, return it
if response.stop_reason == "end_turn":
final_text = next(
(block.text for block in response.content if hasattr(block, "text")),
"No response generated."
)
print(f"\n🤖 Agent: {final_text}")
return final_text
# If Claude wants to use tools, handle each tool call
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f"\n🔧 Tool call: {block.name}({json.dumps(block.input)})")
result = execute_tool(block.name, block.input)
print(f" Result: {result}")
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result)
})
# Feed tool results back into the conversation
self.messages.append({"role": "user", "content": tool_results})
else:
# Unexpected stop reason — bail out to avoid an infinite loop
break
return "Agent loop ended unexpectedly."Step 4: Implement the Tool Execution Layer
Now we wire up the actual Python functions that get called when Claude requests a tool. This is where your real business logic lives — database queries, API calls, calculations, whatever your agent needs to do. I'm using simulated data here so you can run this without any external dependencies.
agent.py — complete working exampleimport anthropic
import os
import json
import math
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
TOOLS = [
{
"name": "calculate",
"description": "Perform mathematical calculations. Use this for any arithmetic, percentages, or numeric operations.",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "A valid Python math expression, e.g. '(150 * 0.08) + 200'"
}
},
"required": ["expression"]
}
},
{
"name": "search_knowledge_base",
"description": "Search a local knowledge base for information about Naples AI services and pricing.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query to look up in the knowledge base"
}
},
"required": ["query"]
}
},
{
"name": "get_weather",
"description": "Get the current weather for a city. Returns temperature and conditions.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'Naples, FL'"
}
},
"required": ["city"]
}
}
]
# -------------------------------------------------------------------
# Tool Implementations
# -------------------------------------------------------------------
def tool_calculate(expression: str) -> str:
"""Safely evaluate a math expression using Python's math module."""
try:
# Only allow safe math operations — no exec or eval of arbitrary code
allowed_names = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
result = eval(expression, {"__builtins__": {}}, allowed_names)
return f"{result}"
except Exception as e:
return f"Calculation error: {str(e)}"
def tool_search_knowledge_base(query: str) -> str:
"""Simulate a knowledge base search with static Naples AI service data."""
kb = {
"chatbot": "Naples AI builds custom AI chatbots starting at $2,500. Deployable on websites, SMS, and WhatsApp.",
"automation": "Intelligent process automation projects typically range from $3,000–$15,000 depending on complexity.",
"real estate": "Naples AI offers real estate listing automation — auto-generate MLS descriptions, social posts, and email campaigns from listing data.",
"seo": "AI-powered SEO content generation service produces 50–200 keyword-optimized articles per month.",
"computer vision": "Computer vision quality control systems for manufacturers start at $8,000 and reduce defect rates by up to 40%.",
"predictive analytics": "Predictive analytics and forecasting models are custom-built. Typical engagement is 4–8 weeks.",
"contact": "Book a free 30-minute strategy call at https://calendly.com/chris-mejias-naplesaiagency/30min"
}
query_lower = query.lower()
for key, value in kb.items():
if key in query_lower:
return value
return "No exact match found. Try searching for: chatbot, automation, real estate, seo, computer vision, predictive analytics, or contact."
def tool_get_weather(city: str) -> str:
"""Return simulated weather data for demo purposes."""
weather_data = {
"naples": {"temp": 84, "condition": "Partly cloudy", "humidity": "72%"},
"miami": {"temp": 88, "condition": "Sunny", "humidity": "68%"},
"fort myers": {"temp": 85, "condition": "Thunderstorms", "humidity": "80%"},
"tampa": {"temp": 82, "condition": "Clear", "humidity": "65%"}
}
city_lower = city.lower().replace(", fl", "").replace(",fl", "").strip()
data = weather_data.get(city_lower, {"temp": 78, "condition": "Unknown", "humidity": "N/A"})
return f"{city}: {data['temp']}°F, {data['condition']}, Humidity: {data['humidity']}"
def execute_tool(tool_name: str, tool_input: dict) -> str:
"""Router that maps tool names to their Python implementations."""
if tool_name == "calculate":
return tool_calculate(tool_input["expression"])
elif tool_name == "search_knowledge_base":
return tool_search_knowledge_base(tool_input["query"])
elif tool_name == "get_weather":
return tool_get_weather(tool_input["city"])
else:
return f"Unknown tool: {tool_name}"
# -------------------------------------------------------------------
# Agent Class
# -------------------------------------------------------------------
class AIAgent:
def __init__(self, system_prompt: str):
self.client = anthropic.Anthropic()
self.model = MODEL
self.tools = TOOLS
self.system_prompt = system_prompt
self.messages = []
def run(self, user_message: str) -> str:
print(f"\n👤 User: {user_message}")
self.messages.append({"role": "user", "content": user_message})
while True:
response = self.client.messages.create(
model=self.model,
max_tokens=4096,
system=self.system_prompt,
tools=self.tools,
messages=self.messages
)
self.messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "end_turn":
final_text = next(
(block.text for block in response.content if hasattr(block, "text")),
"No response generated."
)
print(f"\n🤖 Agent: {final_text}")
return final_text
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f"\n🔧 Tool call: {block.name}({json.dumps(block.input)})")
result = execute_tool(block.name, block.input)
print(f" Result: {result}")
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result)
})
self.messages.append({"role": "user", "content": tool_results})
else:
break
return "Agent loop ended unexpectedly."
# -------------------------------------------------------------------
# Run the Agent
# -------------------------------------------------------------------
if __name__ == "__main__":
system_prompt = """You are a helpful AI assistant for Naples AI, a custom AI solutions agency
in Southwest Florida. You have access to tools for calculations, knowledge base lookups,
and weather data. Always use the appropriate tool when you need specific information.
Be concise and direct in your answers."""
agent = AIAgent(system_prompt=system_prompt)
# Test 1: Multi-step reasoning with calculation
agent.run("If a Naples restaurant automates 3 hours of manual work per day at $18/hr,
how much do they save in a 30-day month?")
# Reset messages between separate conversations
agent.messages = []
# Test 2: Knowledge base lookup
agent.run("What does Naples AI charge for chatbot development?")
agent.messages = []
# Test 3: Multi-tool — weather + calculation
agent.run("What's the weather in Naples FL, and what is 84 squared?")Sample Output
Here's what you'll actually see in your terminal when you run this:
terminal output👤 User: If a Naples restaurant automates 3 hours of manual work per day at $18/hr, how much do they save in a 30-day month?
🔧 Tool call: calculate({"expression": "3 * 18 * 30"})
Result: 1620
🤖 Agent: By automating 3 hours of manual work per day at $18/hour, a Naples restaurant would save $1,620 in a 30-day month. That's nearly $20,000 per year in recovered labor costs.
---
👤 User: What does Naples AI charge for chatbot development?
🔧 Tool call: search_knowledge_base({"query": "chatbot pricing"})
Result: Naples AI builds custom AI chatbots starting at $2,500. Deployable on websites, SMS, and WhatsApp.
🤖 Agent: Naples AI's custom AI chatbots start at $2,500. They can be deployed across websites, SMS, and WhatsApp depending on your business needs.
---
👤 User: What's the weather in Naples FL, and what is 84 squared?
🔧 Tool call: get_weather({"city": "Naples, FL"})
Result: Naples, FL: 84°F, Partly cloudy, Humidity: 72%
🔧 Tool call: calculate({"expression": "84 ** 2"})
Result: 7056
🤖 Agent: Right now in Naples, FL it's 84°F with partly cloudy skies and 72% humidity. And 84 squared is 7,056.How It Works
The agent loop is simpler than it looks. You send a message to Claude along with a list of tool definitions. Claude reads those definitions and decides: do I need more information, or can I answer right now?
If Claude needs a tool, it returns a tool_use stop reason with the tool name and parameters filled in. Your Python code executes the real function, packages the result as a tool_result message, and sends everything back to Claude. Claude then either calls another tool or writes its final answer.
The key insight is that Claude handles all the reasoning — when to use a tool, in what order, and how to combine results. You just define what the tools do and trust the loop.
This same loop works whether you have 3 tools or 30. To add a new capability — say, querying your CRM or pulling from a database — you write one Python function and add one JSON schema to the
TOOLS list. The agent figures out the rest.
Common Errors and Fixes
Error 1: AuthenticationError
Exact error: anthropic.AuthenticationError: Error code: 401 — {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}
Your API key isn't being read. Run echo $ANTHROPIC_API_KEY in your terminal — if it's blank, the variable isn't set in your current session. Re-export it with export ANTHROPIC_API_KEY="sk-ant-..." and try again. Never hardcode API keys in source files.
Error 2: ValueError: messages must alternate between user and assistant roles
Exact error: anthropic.BadRequestError: 400 — messages must alternate between "user" and "assistant" roles
This happens when you append messages in the wrong order — usually when tool results aren't wrapped correctly. Make sure your tool results go into a user role message with "type": "tool_result" exactly as shown in Step 4. The conversation must always follow the pattern: user → assistant → user → assistant.
Error 3: Agent loops forever without answering
Symptom: The terminal keeps printing tool calls endlessly and never prints a final answer.
This usually means your tool is returning an error string that Claude keeps trying to fix by calling the tool again. Add a max_iterations counter to your loop as a safety valve, and make sure your tools return useful error messages when something fails. Also double-check your tool descriptions — if Claude can't match the right tool to the task, it may spiral.