← All guides
Interactive guide Β· 10 levels

Your first agent with AWS Bedrock

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.

structured_output.py
response_tool = [{ "toolSpec": { "name": "classify_ticket", "description": "Classifies a support ticket", "inputSchema": {"json": { "type": "object", "properties": { "category": {"type": "string", "enum": ["bug", "question", "feature"]}, "urgency": {"type": "string", "enum": ["low", "medium", "high"]}, "summary": {"type": "string"}, }, "required": ["category", "urgency", "summary"], }}, } }] resp = bedrock.converse( modelId="anthropic.claude-opus-5-v1:0", messages=[{"role": "user", "content": [{"text": "The app crashes when uploading a profile photo, urgent"}]}], toolConfig={"tools": response_tool, "toolChoice": {"tool": {"name": "classify_ticket"}}}, ) data = resp["output"]["message"]["content"][0]["toolUse"]["input"] print(data) # {'category': 'bug', 'urgency': 'high', 'summary': '...'}
Challenge

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.

Level 8 Β· Orchestrator

Multi-agent collaboration: supervisor + collaborators

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

final architecture
Client β”‚ β–Ό API Gateway β”‚ β–Ό Supervisor Agent (Bedrock Agents, on AgentCore Runtime) β”‚ β–Ό SQS Queue (decouples traffic spikes) β”‚ β”œβ”€β”€β–Ά Cross-region inference profile ──▢ [Sales Collaborator] β”œβ”€β”€β–Ά Cross-region inference profile ──▢ [Support Collaborator] └──▢ Cross-region inference profile ──▢ [Billing Collaborator] β”‚ β–Ό CloudWatch + X-Ray (end-to-end traces)

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.