Wire the real model — swap fake_llm for the Anthropic SDK shape — step 1 of 9
Same loop, real model
Lesson 1 wrote the agent loop on top of fake_llm — a function that
returned canned response dicts. The whole point of building it that
way was that the loop is the load-bearing part. The model is
swappable.
Here's what the swap actually looks like. Two lines change:
import os
from anthropic import Anthropic
client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
That's it. The messages.create(...) call returns a response object
whose .stop_reason, .content, .content[i].type, and
.content[i].text access patterns are identical to the dict-shaped
mock you built. The loop you wrote in lesson 1 runs on real
production traffic with no other code changes.
Three pieces this lesson locks in:
1. The key lives in the environment
os.getenv("ANTHROPIC_API_KEY") — never hardcoded, never in source,
never committed. Chapter 18 set this up; this is the chapter where
it actually matters because the request goes out to a real billed
endpoint.
The two reflexes you'll fight:
- Putting the key in the source for "just this one test." It ends up in git history. You rotate the key. Two days later you notice the bill. By then it's too late.
- Putting the key in
os.environ["ANTHROPIC_API_KEY"](bracket access). This raisesKeyErrorif the env var is missing.os.getenvreturnsNoneand lets you give a clear error upstream. Alwaysgetenv, never bracket.
2. The SDK response is OBJECTS, not dicts
The mock you wrote in lesson 1 used dicts (response["stop_reason"],
response["content"][0]["text"]). The real SDK uses object access
(response.stop_reason, response.content[0].text). Almost
identical, slightly different.
Every block in response.content has a .type field. For text
blocks, .text. For tool_use blocks, .name, .input, .id. You
must check .type before reading the type-specific fields, or
you'll get AttributeError on the first tool-using turn.
3. The loop doesn't care
The genuinely good news: the loop you wrote in lesson 1 — the
while, the stop_reason branch, the tool dispatch, the
messages.append(...) — all runs unchanged. The mock you'll build
in step 8 has exactly the real SDK's attribute shape. Lift the
loop, change the call site, ship it.
What you'll build
A MockResponse with MockTextBlock and MockToolUseBlock classes
whose attribute access matches real anthropic.types.Message. Then
a fix for the most common KeyError bug (bracket access on env
vars). Then a fix for the AttributeError bug (reading .text on a
tool_use block). Then a real shape adapter that takes either a real
SDK response or a mock and runs the loop unchanged.
Wire the real model — swap fake_llm for the Anthropic SDK shape — step 1 of 9
Same loop, real model
Lesson 1 wrote the agent loop on top of fake_llm — a function that
returned canned response dicts. The whole point of building it that
way was that the loop is the load-bearing part. The model is
swappable.
Here's what the swap actually looks like. Two lines change:
import os
from anthropic import Anthropic
client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
That's it. The messages.create(...) call returns a response object
whose .stop_reason, .content, .content[i].type, and
.content[i].text access patterns are identical to the dict-shaped
mock you built. The loop you wrote in lesson 1 runs on real
production traffic with no other code changes.
Three pieces this lesson locks in:
1. The key lives in the environment
os.getenv("ANTHROPIC_API_KEY") — never hardcoded, never in source,
never committed. Chapter 18 set this up; this is the chapter where
it actually matters because the request goes out to a real billed
endpoint.
The two reflexes you'll fight:
- Putting the key in the source for "just this one test." It ends up in git history. You rotate the key. Two days later you notice the bill. By then it's too late.
- Putting the key in
os.environ["ANTHROPIC_API_KEY"](bracket access). This raisesKeyErrorif the env var is missing.os.getenvreturnsNoneand lets you give a clear error upstream. Alwaysgetenv, never bracket.
2. The SDK response is OBJECTS, not dicts
The mock you wrote in lesson 1 used dicts (response["stop_reason"],
response["content"][0]["text"]). The real SDK uses object access
(response.stop_reason, response.content[0].text). Almost
identical, slightly different.
Every block in response.content has a .type field. For text
blocks, .text. For tool_use blocks, .name, .input, .id. You
must check .type before reading the type-specific fields, or
you'll get AttributeError on the first tool-using turn.
3. The loop doesn't care
The genuinely good news: the loop you wrote in lesson 1 — the
while, the stop_reason branch, the tool dispatch, the
messages.append(...) — all runs unchanged. The mock you'll build
in step 8 has exactly the real SDK's attribute shape. Lift the
loop, change the call site, ship it.
What you'll build
A MockResponse with MockTextBlock and MockToolUseBlock classes
whose attribute access matches real anthropic.types.Message. Then
a fix for the most common KeyError bug (bracket access on env
vars). Then a fix for the AttributeError bug (reading .text on a
tool_use block). Then a real shape adapter that takes either a real
SDK response or a mock and runs the loop unchanged.