Build a working agent in ~80 lines of Python. No frameworks. Just anthropic and a loop.
Agents Module 1 Python
This lesson is the one time we build the whole thing by hand. Modules 2–5 hand it back to a framework.
A single-file Python script — agent.py — that:
get_weather, get_clothing_advice — same as L01 lab),stop_reason == "end_turn" or turn cap or cost cap,import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from env
response = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=1024,
system="You are a helpful assistant.",
messages=[{"role": "user", "content": "Hello"}],
)
print(response.content[0].text)
Every agent we build in this course calls messages.create in a loop. That's it. Everything else is plumbing.
TOOLS = [
{
"name": "get_weather",
"description": "Get current weather for a city.",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
# ... get_clothing_advice
]
The LLM sees these tool schemas on every call. Names, descriptions, and JSON Schemas are how it decides what to call and what arguments to pass.
stop_reason values| Value | Meaning | Loop action |
|---|---|---|
end_turn | LLM produced a final answer | exit loop |
tool_use | LLM wants to call a tool | execute, append result, continue |
max_tokens | hit max_tokens limit | extend or fail |
stop_sequence | hit a stop sequence | usually exit |
The whole agent loop is a switch statement on this one field.
messages = [{"role": "user", "content": user_q}]
for turn in range(MAX_TURNS):
resp = client.messages.create(
model=MODEL,
system=SYSTEM,
tools=TOOLS,
messages=messages,
max_tokens=1024,
)
if resp.stop_reason == "end_turn":
return resp.content[-1].text
if resp.stop_reason == "tool_use":
# extract tool_use block, run tool, append result
...
raise RuntimeError("turn cap reached")
Eleven lines. Everything else in the file is plumbing around this shape.
tool_use blocktool_uses = [b for b in resp.content if b.type == "tool_use"]
for tu in tool_uses:
result = TOOL_FUNCS[tu.name](**tu.input)
messages.append({"role": "assistant", "content": resp.content})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tu.id,
"content": str(result),
}],
})
The assistant message must contain the full resp.content block list, then a user message carries the tool_result blocks back. The runtime is pretending to be the user when reporting tool output.
def get_weather(city: str) -> str:
# Deterministic fake — real version would call a weather API
return f"{city}: 4°C, light rain"
def get_clothing_advice(temp_c: int, condition: str) -> str:
if temp_c < 10:
return "Wear a warm jacket. " + ("Bring an umbrella." if "rain" in condition else "")
return "Light layers should be fine."
TOOL_FUNCS = {
"get_weather": get_weather,
"get_clothing_advice": get_clothing_advice,
}
Deterministic mocks keep the lab cheap, fast, and reproducible. Swap in real APIs later — the loop doesn't change.
total_input = total_output = 0
# inside loop, after each messages.create():
total_input += resp.usage.input_tokens
total_output += resp.usage.output_tokens
cost_usd = (total_input * 3 + total_output * 15) / 1_000_000 # Sonnet pricing
if cost_usd > MAX_COST_USD:
raise RuntimeError("cost cap exceeded")
The cost cap is the second stop condition — it catches what MAX_TURNS misses: one expensive turn with a huge context window.
from anthropic import APIStatusError
import time
for attempt in range(3):
try:
resp = client.messages.create(...)
break
except APIStatusError as e:
if e.status_code >= 500 and attempt < 2:
time.sleep(2 ** attempt)
continue
raise
Three attempts, exponential backoff, only on 5xx. 4xx errors are bugs in your request — retrying them just burns tokens.
agent.py.anthropic, os, time.python agent.py "your question".All of these are what the frameworks in Modules 2–5 give you. Now you'll know what they're really doing under the hood — and the cost of writing them yourself.
messages.create until end_turn. Everything else is dispatch, budgets, and retries.