By the end of this lab you will have written a ReAct agent — the Reason + Act loop behind every modern AI agent — from scratch, in about forty lines of Python. It reasons about what it needs, calls tools to get it, reads the results, and repeats until it can answer. And it prints its whole trace, so you watch the loop turn.
Before you start
Python 3.10+, an Anthropic API key (export ANTHROPIC_API_KEY=...), about 40 minutes.
uv init react-agent && cd react-agent
uv add anthropic
# or: pip install anthropic
Give the agent two tools
An agent is only as capable as its tools. We give it two tiny ones — a fact lookup and a calculator — so it can answer a question no single lookup could. Create agent.py:
def lookup(person: str) -> str:
"""Return birth and death years for a well-known person."""
facts = {
"isaac newton": "Isaac Newton: born 1643, died 1727.",
"albert einstein": "Albert Einstein: born 1879, died 1955.",
}
return facts.get(person.lower().strip(), "No record found.")
def calculator(expression: str) -> str:
"""Evaluate a simple arithmetic expression like '1727 - 1643'."""
return str(eval(expression, {"__builtins__": {}})) # tiny sandbox for the demo
Describe the tools to the model
The model can only use a tool if it knows the tool exists and what shape its input takes. That is what a tool schema is.
tools = [
{
"name": "lookup",
"description": "Look up the birth and death years of a well-known person.",
"input_schema": {
"type": "object",
"properties": {"person": {"type": "string"}},
"required": ["person"],
},
},
{
"name": "calculator",
"description": "Evaluate a simple arithmetic expression, e.g. 1727 - 1643.",
"input_schema": {
"type": "object",
"properties": {"expression": {"type": "string"}},
"required": ["expression"],
},
},
]
handlers = {
"lookup": lambda i: lookup(i["person"]),
"calculator": lambda i: calculator(i["expression"]),
}
Write the loop — this is ReAct
Here is the whole thing. The for loop is the ReAct loop: the model's text is the Thought, a tool_use block is the Action, the tool_result you feed back is the Observation, and a turn with no tool call is the Finish.
from anthropic import Anthropic
client = Anthropic()
def agent(question: str, max_steps: int = 8) -> str:
messages = [{"role": "user", "content": question}]
for _ in range(max_steps): # the step cap — a confused agent can't loop forever
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
tools=tools,
messages=messages,
)
messages.append({"role": "assistant", "content": resp.content})
# THOUGHT: whatever the model reasoned out loud this turn
for b in resp.content:
if b.type == "text" and b.text.strip():
print("🧠 THOUGHT:", b.text.strip())
if resp.stop_reason != "tool_use":
return next((b.text for b in resp.content if b.type == "text"), "")
# ACTION + OBSERVATION: run each tool, feed the results back
results = []
for b in resp.content:
if b.type == "tool_use":
print(f"🔧 ACTION: {b.name}({b.input})")
observation = handlers[b.name](b.input)
print("👀 OBSERVATION:", observation)
results.append({
"type": "tool_result",
"tool_use_id": b.id,
"content": observation,
})
messages.append({"role": "user", "content": results})
return "Stopped: hit the step limit before finishing."
print("\n✅ ANSWER:", agent(
"Who lived longer, Isaac Newton or Albert Einstein, and by how many years?"
))
Run it and watch the loop
uv run agent.py
You will see the agent investigate rather than guess — something like:
🧠 THOUGHT: I need each man's lifespan. Let me look up Newton first.
🔧 ACTION: lookup({'person': 'Isaac Newton'})
👀 OBSERVATION: Isaac Newton: born 1643, died 1727.
🔧 ACTION: lookup({'person': 'Albert Einstein'})
👀 OBSERVATION: Albert Einstein: born 1879, died 1955.
🧠 THOUGHT: Newton lived 1727-1643 years; Einstein 1955-1879. Let me compute both.
🔧 ACTION: calculator({'expression': '1727 - 1643'})
👀 OBSERVATION: 84
🔧 ACTION: calculator({'expression': '1955 - 1879'})
👀 OBSERVATION: 76
✅ ANSWER: Newton lived longer — 84 years to Einstein's 76, a difference of 8 years.
Where to go next
- Add a real tool — swap
lookupfor a live API (start with something keyless) and watch the agent reach into the real world. - Handle failure — return a helpful error string from a tool and see the agent recover and try another approach.
- Go deeper — the ReAct Loop course covers the research and failure modes; Harness Engineering and AI Ops cover making this production-safe.