From your first Converse API call in Python, to a production architecture with multi-agent Bedrock Agents, AgentCore, and cross-region load balancing.
0 / 10 completed
Level 1 Β· Foundations
Your first call with the Converse API
Goal: set up boto3, pick a model in Bedrock, and make your first call with the Converse API.
Amazon Bedrock gives you access to models from Anthropic, Meta, Amazon, and others through a single API managed by AWS. The Converse API is the modern, recommended way to talk to any Bedrock model: one unified interface, regardless of which provider is behind it.
1. Install boto3 and configure credentials
TERMINAL
pip install boto3
aws configure # or the AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY env vars
# In the Bedrock console, enable access to the model you're going to use
2. Your first call
agent.py
import boto3
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
response = bedrock.converse(
modelId="anthropic.claude-opus-5-v1:0",
messages=[
{"role": "user", "content": [{"text": "What is an AI agent, in one sentence?"}]}
],
inferenceConfig={"maxTokens": 300, "temperature": 0.3},
)
print(response["output"]["message"]["content"][0]["text"])
The Converse API always returns the same response shape regardless of the model β you can switch modelId from Claude to Llama or Nova without touching the rest of the code.
Challenge
Try the same prompt with two different modelId values available on your account and compare response length and style.
Level 2 Β· Personality
System prompt and inference parameters
Goal: control the model's behavior with system and inferenceConfig.
The Converse API separates the system prompt from the conversation messages β that way the model clearly distinguishes between "who you are" and "what you're being asked".
agent.py
response = bedrock.converse(
modelId="anthropic.claude-opus-5-v1:0",
system=[{
"text": (
"You are a technical support assistant. Reply in English, "
"briefly, with numbered steps. If you don't know something, say so."
)
}],
messages=[
{"role": "user", "content": [{"text": "The app crashes when I open the camera"}]}
],
inferenceConfig={"maxTokens": 400, "temperature": 0.2, "topP": 0.9},
)
temperature β precision (low) vs. creativity (high)
maxTokens β length limit, key for controlling cost
topP β controls the diversity of token sampling
Challenge
Write a system prompt that forces the model to always answer with a single-line JSON object, with no extra text.
Level 3 Β· Tools
Tool use with toolConfig
Goal: define a tool with toolSpec and handle the Converse API's function-calling loop.
Unlike a high-level agent SDK, with the Converse API you manage the loop yourself: the model returns a toolUse block, your code runs the real function, and you send the result back as a toolResult on the next turn.
agent_tools.py
tool_list = [{
"toolSpec": {
"name": "current_weather",
"description": "Returns the current weather for a city",
"inputSchema": {"json": {
"type": "object",
"properties": {"city": {"type": "string", "description": "Name of the city"}},
"required": ["city"],
}},
}
}]
messages = [{"role": "user", "content": [{"text": "What's the weather like in Guatemala City?"}]}]
resp = bedrock.converse(
modelId="anthropic.claude-opus-5-v1:0",
messages=messages,
toolConfig={"tools": tool_list},
)
block = resp["output"]["message"]["content"][0]
if "toolUse" in block:
city = block["toolUse"]["input"]["city"]
result = "72Β°F, partly cloudy" # your real function would go here
messages.append(resp["output"]["message"])
messages.append({
"role": "user",
"content": [{"toolResult": {
"toolUseId": block["toolUse"]["toolUseId"],
"content": [{"text": result}],
}}],
})
resp = bedrock.converse(modelId="anthropic.claude-opus-5-v1:0", messages=messages, toolConfig={"tools": tool_list})
print(resp["output"]["message"]["content"][0]["text"])
This manual pattern is exactly what frameworks like Strands or LangChain automate for you β understanding it helps you debug when something breaks in the higher-level layers.
Challenge
Add a second tool (a calculator) to tool_list and try a prompt that requires both in the same conversation.
Level 4 Β· Memory
Conversation history
Goal: keep context across turns by managing the messages list yourself.
The Converse API is stateless: it remembers nothing between calls. Conversational memory is your application's responsibility β you simply accumulate previous messages in the same list you send with every call.
chat_with_memory.py
history = []
def talk(text):
history.append({"role": "user", "content": [{"text": text}]})
resp = bedrock.converse(modelId="anthropic.claude-opus-5-v1:0", messages=history)
model_message = resp["output"]["message"]
history.append(model_message)
return model_message["content"][0]["text"]
print(talk("I'm looking for a laptop for graphic design"))
print(talk("Which of the ones you mentioned is the cheapest?"))
In production, you don't accumulate history indefinitely β you truncate or summarize it every so many turns so you don't blow past the context window or pay extra for repeated tokens.
Challenge
Modify talk() so that once the history exceeds 10 messages, it summarizes the first 6 into a single system message and replaces them.
Level 5 Β· Structured output
Forcing JSON with a response tool
Goal: get validated data instead of free text, using a tool as a "forced schema".
Bedrock has no native "structured output" parameter in the Converse API β the standard pattern is to define a tool whose only purpose is to force the output schema, and require the model to use it with toolChoice.
Validate the returned input against a Pydantic model to get type guarantees on top of the JSON schema.
Level 6 Β· Streaming
ConverseStream for live responses
Goal: use converse_stream() to display tokens as they arrive.
For production chats and APIs, converse_stream returns an iterator of events instead of waiting for the full response.
stream.py
resp = bedrock.converse_stream(
modelId="anthropic.claude-opus-5-v1:0",
messages=[{"role": "user", "content": [{"text": "Explain what RAG is in 3 steps"}]}],
)
for event in resp["stream"]:
if "contentBlockDelta" in event:
delta = event["contentBlockDelta"]["delta"]
if "text" in delta:
print(delta["text"], end="", flush=True)
The events also include messageStart, contentBlockStart (useful for detecting the start of a toolUse), and messageStop with the finish reason.
Challenge
Wrap this stream in an async generator and expose it from a FastAPI endpoint with StreamingResponse.
Level 7 Β· Bedrock Agents
From the Converse API to Bedrock Agents
Goal: create a managed agent with the bedrock-agent client, which handles the tool loop for you.
Bedrock Agents is AWS's managed layer on top of everything you built by hand in the previous levels: it keeps the reasoning loop running, executes action groups (your tools), and stores session state automatically.
create_agent.py
import boto3
agent_client = boto3.client("bedrock-agent", region_name="us-east-1")
response = agent_client.create_agent(
agentName="support-agent",
foundationModel="anthropic.claude-opus-5-v1:0",
instruction=(
"You are a technical support agent. Use the available tools "
"to check ticket status before responding."
),
agentResourceRoleArn="arn:aws:iam::123456789012:role/BedrockAgentRole",
)
agent_id = response["agent"]["agentId"]
print(f"Agent created: {agent_id}")
# Action Groups (your tools) are added next, and the agent is invoked with
# the bedrock-agent-runtime client -> invoke_agent()
Challenge
Define an Action Group with a Lambda function that looks up a mock ticket status, and test invoke_agent() with a question that triggers it.
Goal: build a supervisor agent that delegates to specialized collaborator agents, using Bedrock's native functionality.
Since 2025, Bedrock natively supports multi-agent collaboration: a supervisor agent coordinates a network of specialized collaborator agents, each solving its part of the problem.
orchestrator.py
agent_client.associate_agent_collaborator(
agentId=supervisor_agent_id,
agentVersion="DRAFT",
agentDescriptor={"aliasArn": sales_agent_alias_arn},
collaboratorName="sales-specialist",
collaborationInstruction="Delegate pricing and product questions to this collaborator.",
relayConversationHistory="TO_COLLABORATOR",
)
agent_client.associate_agent_collaborator(
agentId=supervisor_agent_id,
agentVersion="DRAFT",
agentDescriptor={"aliasArn": support_agent_alias_arn},
collaboratorName="support-specialist",
collaborationInstruction="Delegate bug and technical error questions to this collaborator.",
relayConversationHistory="TO_COLLABORATOR",
)
# The supervisor decides, on every turn, which collaborator to delegate to
This is AWS's native equivalent of the "orchestrator + specialists" pattern you'd otherwise build by hand with LangGraph or Strands β the difference is that here AWS manages the routing, versioning, and lifecycle of each collaborator agent.
Challenge
Add a third billing collaborator and check the CloudWatch logs to see which one the supervisor delegates to for an ambiguous question.
Level 9 Β· Production
Observability with AgentCore
Goal: instrument production traces and understand AgentCore Runtime as a deployment layer.
Amazon Bedrock AgentCore is the serverless runtime for deploying agents (built with Bedrock Agents, LangGraph, Strands, or CrewAI) with built-in observability: every reasoning step, tool call, and model interaction gets traced.
enable observability
# 1. Enable CloudWatch Transaction Search (once per account)
aws xray update-trace-segment-destination --destination CloudWatchLogs
# 2. Deploy the agent directly from code (Python 3.10-3.13)
agentcore configure --entrypoint agent.py
agentcore launch
# AgentCore automatically instruments traces, tool calls, and model decisions,
# visible in CloudWatch's GenAI Observability dashboard
Guardrails
Bedrock Guardrails to filter harmful or off-topic content before/after the model
maxTokens limits and timeouts per invocation
Retries with exponential backoff on ThrottlingException
Challenge
Set up a basic Guardrail that blocks topics outside your domain and test it against your Level 7 agent.
Level 10 Β· Final architecture
AgentCore Runtime + cross-region load balancing
Goal: assemble a scalable production architecture: supervisor + collaborators, deployed on AgentCore, with distributed load.
AgentCore Runtime already handles horizontal auto-scaling for each agent for you (it's serverless). What this level adds is distributing inference load across regions and decoupling traffic spikes with a queue.
1. Cross-region inference profiles
cross-region load balancing
response = bedrock.converse(
# A cross-region inference profile automatically spreads the load
# across multiple AWS regions where the model is available
modelId="us.anthropic.claude-opus-5-v1:0",
messages=[{"role": "user", "content": [{"text": "..."}]}],
)
# AWS decides which region to route each request to based on available
# capacity, reducing throttling during traffic spikes
2. A queue to decouple the supervisor from the collaborators
With that, you've got the full path: from a one-line converse() call in Level 1, to a multi-region architecture with a supervisor, specialized collaborators, and end-to-end observability, all managed by AWS.
Final challenge
Deploy the Level 8 supervisor on AgentCore Runtime and measure latency with and without the cross-region inference profile under simulated load.