Build a Multi-Tool AI Agent in Python
Three tools, one agent loop, zero external APIs — and a result that actually matters.
Hey All, Real John Here, this week, I’m doing something a little different. I’m showing you actual code. Not theory, but real working code. I’ve read so many posts, watched so many videos, and they all talk about… “theory”… or the things you CAN do with AI. My own newsletter was part of that.. so let’s get you actually building something. So the only thing you need is an Anthropic API key.
8 of Hearts (if you know you know)
Everyone is talking about AI agents. Every conference has a panel about them. Every VC deck has the word “agentic” in it somewhere. But very few people actually show you how to build one from scratch.
Today we’re fixing that.
By the end of this newsletter you’ll have a working AI agent running on your machine — one that uses three tools, chains them together automatically, and produces a real business output. Not a demo. Not a screenshot. Code that runs.
Let’s go.
First — what actually IS an agent?
Here’s the simplest explanation I’ve got.
A regular AI call is a question and an answer. You ask, it responds. Done. One round trip.
An agent is different. An agent can do things in between the question and the answer. It can look something up, run a calculation, call an API, check a file — and then use the result to keep going. It’s the difference between asking a colleague a question and asking them to go figure something out and come back with an answer.
That loop — ask, act, observe, repeat — is the core pattern behind every agent you’ve heard about. AutoGPT, Devin, whatever the hot thing is this week. It’s all variations of the same idea.
Now let’s build one.
Step 1: Get your API key
You’ll need an Anthropic API key. Here’s how to get one:
Go to console.anthropic.com and create an account (it’s free to sign up)
Once logged in, click API Keys in the left sidebar
Click Create Key, give it a name, and copy it somewhere safe — you won’t see it again
New accounts get free credits to start. Enough to run this tutorial many times over.
Once you have your key, set it as an environment variable so you’re not hardcoding it anywhere:
# Mac/Linux
export ANTHROPIC_API_KEY="sk-ant-your-key-here"
# Windows (PowerShell)
$env:ANTHROPIC_API_KEY="sk-ant-your-key-here"
Then install the SDK:
pip install anthropic
That’s the entire setup. No Docker, no cloud account, no database. Just Python and an API key.
Step 2: Understand the three-part pattern
Before we write the agent, here’s the mental model you need:
Tools are plain Python functions. Claude never calls them directly — it requests them by name with arguments, and you run the actual function yourself and feed the result back.
Descriptions are how Claude knows what each tool does. The better you describe a tool, the smarter Claude uses it. This is where most beginners leave performance on the table.
The loop keeps running until Claude says it’s done. Each iteration: Claude decides what to do next, you execute it, you report back. Repeat.
That’s it. Now let’s see it in code.
Step 3: The scenario
We’re building a product analytics agent for a small SaaS business. It can pull product data, run calculations, and generate a formatted report.
You’ll ask it one natural language question — something like “Which plan has the worst unit economics, and what would a 15% price increase do to Pro Plan revenue?” — and it will figure out the sequence of steps to answer it.
Step 4: The full code
Create a file called agent.py and paste this in:
import anthropic
import json
client = anthropic.Anthropic() # Reads ANTHROPIC_API_KEY from environment
# ------------------------------------
# Our fake "database"
# ------------------------------------
PRODUCT_DATA = {
"basic_plan": {
"name": "Basic Plan",
"monthly_subscribers": 1240,
"price": 9.99,
"churn_rate": 0.08,
"cac": 42.00
},
"pro_plan": {
"name": "Pro Plan",
"monthly_subscribers": 380,
"price": 29.99,
"churn_rate": 0.04,
"cac": 95.00
},
"enterprise_plan": {
"name": "Enterprise Plan",
"monthly_subscribers": 47,
"price": 199.99,
"churn_rate": 0.02,
"cac": 420.00
}
}
# ------------------------------------
# Tool 1: Pull product data
# ------------------------------------
def get_product_data(product_id: str) -> str:
if product_id == "all":
return json.dumps(PRODUCT_DATA)
if product_id in PRODUCT_DATA:
return json.dumps(PRODUCT_DATA[product_id])
return json.dumps({"error": f"Product '{product_id}' not found"})
# ------------------------------------
# Tool 2: Run a math calculation
# ------------------------------------
def calculate_metrics(expression: str) -> str:
try:
result = eval(expression, {"__builtins__": {}}, {})
return str(round(float(result), 2))
except Exception as e:
return f"Calculation error: {e}"
# ------------------------------------
# Tool 3: Generate a formatted report
# ------------------------------------
def generate_report(title: str, findings: list, recommendation: str) -> str:
report = f"\n{'='*50}\n"
report += f" {title.upper()}\n"
report += f"{'='*50}\n\n"
for finding in findings:
report += f" • {finding}\n"
report += f"\nRECOMMENDATION:\n {recommendation}\n"
report += f"{'='*50}\n"
return report
# ------------------------------------
# Tell Claude what tools exist
# ------------------------------------
tools = [
{
"name": "get_product_data",
"description": "Retrieves product data. Pass 'all' to get all products, or a specific product_id like 'basic_plan', 'pro_plan', or 'enterprise_plan'.",
"input_schema": {
"type": "object",
"properties": {
"product_id": {
"type": "string",
"description": "The product ID to look up, or 'all' for everything"
}
},
"required": ["product_id"]
}
},
{
"name": "calculate_metrics",
"description": "Evaluates a Python math expression. Use for revenue calculations, churn projections, LTV estimates, or any arithmetic. Returns a rounded number.",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "A math expression, e.g. '380 * 29.99 * 12'"
}
},
"required": ["expression"]
}
},
{
"name": "generate_report",
"description": "Formats and outputs a structured business report. Call this LAST, only after all data has been gathered and calculations are complete.",
"input_schema": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Report title"
},
"findings": {
"type": "array",
"items": {"type": "string"},
"description": "List of key findings, each as a concise string"
},
"recommendation": {
"type": "string",
"description": "A single clear action recommendation based on the findings"
}
},
"required": ["title", "findings", "recommendation"]
}
}
]
# ------------------------------------
# The agent loop
# ------------------------------------
def run_agent(user_message: str):
print(f"\nUser: {user_message}\n")
messages = [{"role": "user", "content": user_message}]
# Map tool names to actual Python functions
tool_functions = {
"get_product_data": get_product_data,
"calculate_metrics": calculate_metrics,
"generate_report": generate_report
}
turn = 0
while True:
turn += 1
print(f"[Turn {turn}] Asking Claude what to do next...")
response = client.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=2048,
tools=tools,
messages=messages
)
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
fn = tool_functions.get(block.name)
if fn:
print(f" → Calling: {block.name}({block.input})")
result = fn(**block.input)
preview = result[:120] + "..." if len(result) > 120 else result
print(f" ← Result: {preview}")
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
# Add Claude's response + our tool results back into the conversation
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
else:
# Claude is done — print the final answer
for block in response.content:
if hasattr(block, "text"):
print(block.text)
print(f"\n[Done in {turn} turns]")
break
# ------------------------------------
# Run it
# ------------------------------------
run_agent(
"Analyze my product lineup. Which plan has the worst LTV-to-CAC ratio? "
"Calculate what annual revenue looks like if the Pro Plan price goes up 15%. "
"Then give me a formatted report with your findings and a clear recommendation."
)
Step 5: Run it and watch what happens
python agent.py
You’ll see something like this:
User: Analyze my product lineup...
[Turn 1] Asking Claude what to do next...
→ Calling: get_product_data({'product_id': 'all'})
← Result: {"basic_plan": {"name": "Basic Plan", "monthly_subscribers": 1240...
[Turn 2] Asking Claude what to do next...
→ Calling: calculate_metrics({'expression': '9.99 / 0.08'})
← Result: 124.88
→ Calling: calculate_metrics({'expression': '29.99 / 0.04'})
← Result: 749.75
→ Calling: calculate_metrics({'expression': '199.99 / 0.02'})
← Result: 9999.5
[Turn 3] Asking Claude what to do next...
→ Calling: calculate_metrics({'expression': '29.99 * 1.15 * 380 * 12'})
← Result: 158214.96
[Turn 4] Asking Claude what to do next...
→ Calling: generate_report({'title': 'Product Lineup Analysis', 'findings': [...]
==================================================
PRODUCT LINEUP ANALYSIS
==================================================
• Basic Plan LTV-to-CAC ratio is 2.97x — the weakest in the lineup
• Pro Plan LTV-to-CAC is 7.89x — strongest unit economics by far
• Enterprise LTV is exceptional but volume is too small to drive growth
• A 15% price increase on Pro Plan yields $158,214 annually vs $136,742 today
RECOMMENDATION:
Raise Pro Plan to $34.49, shift acquisition spend from Basic to Pro,
and set an enterprise expansion target of 20 new accounts this quarter.
==================================================
[Done in 4 turns]
You didn’t write any of that logic. You didn’t tell Claude to calculate LTV as price / churn_rate. You didn’t tell it to compare ratios across all three plans. You asked a business question and it figured out the sequence.
The things worth noticing
Look at Turn 2. Claude called calculate_metrics three times in the same turn — batching all the LTV calculations before moving on. It figured out it needed all three numbers before it could compare them. That’s Claude planning ahead, not just reacting.
The "Call this LAST" hint in generate_report actually works. Without it, Claude sometimes tries to generate the report before it has finished calculating. One sentence in a tool description can completely change agent behavior. Descriptions are not documentation — they’re instructions.
The loop is the whole thing. Every sophisticated agent you read about is a variation of this same while loop. The tools change. The complexity scales. The pattern stays the same.
I’ve tested this code a few times and it works great on a Mac and Windows, but with Windows, you do need to setup a little more, Python. I recommend using GitBash on Windows or the WSL platform (windows subsystem for Linux). It makes it easier. I tried to keep this self-contained but useful enough for you to learn and run with it.
Where to take this next
The code you just wrote is more extensible than it looks. Some ideas:
Replace PRODUCT_DATA with a real database query and you’ve got a live analytics agent. Replace generate_report with a function that sends a Slack message or fires off an email and now it takes action. Add a search_web tool and it can pull in external context before making recommendations. Add a write_file tool and it can save its own output.
The agent loop doesn’t care what the tools do. It just runs whatever you hand it.
That’s the unlock. Once you internalize this pattern, every integration you’ve ever built becomes a potential tool for an agent to orchestrate.
Now go run the code.
Execution beats everything.
— John
Questions? Hit reply — I read every one, and usually respond 🤣




This was helpful, thanks! To use this example with VertexAI
# you won't need the ANTHROPIC_API_KEY variable
# replace
client = anthropic.Anthropic() # Reads ANTHROPIC_API_KEY from environment
# with this
client = anthropic.AnthropicVertex(
project_id="<your-gcp-project>",
region="<your-region>"
)