AI EngineeringZero to ProductionHome·About·Contact
AWS AI Automation · Chapter W5

Bedrock Agents

A Bedrock Agent runs the W3 tool loop for you: declare action groups (Lambda), attach a Knowledge Base, and AWS orchestrates the calls and keeps session state.

⏱️ ~2 hours🧪 3 labs🎯 Advanced
⚙️ To run this for realThe code here is complete and correct as written. To actually execute it you'll need:
  • AWS credentials (aws configure) + Bedrock model access enabled in your region + pip install boto3
  • AWS credentials (aws configure) + pip install boto3
Reading and learning works without any of this — run when you're ready. IDs/ARNs in examples are placeholders; swap in your own.

Learning objectives

  • Explain how a Bedrock Agent relates to the manual tool loop from W3.
  • Define action groups backed by Lambda functions.
  • Attach a Knowledge Base so the agent can retrieve and act.
  • Invoke the agent and trace its orchestration + session state.
▶ Runnable companionEvery code block in this lesson is also a standalone file under code/aws5-bedrock-agents/ in the course, with a README on how to run it. Read here, run there — no need to copy-paste.

An Agent is the W3 loop, managed essential

In W3 you ran the tool loop by hand: call model → get tool_use → run tool → return result → repeat. A Bedrock Agent runs that loop for you. You declare action groups (tools, implemented as Lambda) and optionally attach a Knowledge Base; AWS orchestrates, maintains session memory, and calls your Lambdas.

Same safety lesson appliesThe human-in-the-loop gate from Ch 8 still matters: an agent that can call issue_refund needs a confirmation path. W6 (Guardrails) is one layer; your Lambda authorization is another.

Define an action group (Lambda-backed tool) essential

An action group maps an OpenAPI-ish schema to a Lambda. When the agent decides to act, Bedrock invokes the Lambda with the parameters and feeds the return value back into the loop.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Lab W5.1
action_lambda.py# The Lambda that backs an action group. Bedrock calls it with the
# agent's chosen action + parameters, and expects a structured response.
def lambda_handler(event, context):
    action = event["actionGroup"]
    api_path = event["apiPath"]              # e.g. "/pods/{name}/status"
    params = {p["name"]: p["value"] for p in event.get("parameters", [])}

    if api_path == "/pods/status":
        result = {"phase": "CrashLoopBackOff", "restarts": 7}
    else:
        result = {"error": "unknown action"}

    return {
        "messageVersion": "1.0",
        "response": {
            "actionGroup": action,
            "apiPath": api_path,
            "httpStatusCode": 200,
            "responseBody": {"application/json": {"body": str(result)}},
        },
    }
▶ How this works

This is the Lambda function that does the real work behind one of the agent's tools (an action group). You never call it yourself — Bedrock calls it for you whenever the agent decides to take an action. Bedrock passes in a big dictionary called event describing which action and what parameters, and your job is to do the work and hand back a result in the exact shape Bedrock expects.

  1. def lambda_handler(event, context): is the fixed entry point AWS Lambda always calls. event holds the request Bedrock sent; context is runtime info you can usually ignore here.
  2. action = event["actionGroup"] and api_path = event["apiPath"] read which tool and which operation the agent chose (e.g. the path /pods/{name}/status).
  3. params = {p["name"]: p["value"] for p in event.get("parameters", [])} is a dictionary comprehension: Bedrock sends parameters as a list of {name, value} pairs, and this line flips them into an easy-to-use {name: value} dictionary. .get(..., []) means "use an empty list if there are no parameters" so it never crashes.
  4. The if api_path == "/pods/status": block is where your logic lives. Here it just returns a fake pod status; anything Bedrock didn't recognise falls through to the else and returns an "unknown action" error.
  5. The big return {...} is the contract Bedrock requires. It must include messageVersion and a response object echoing back actionGroup/apiPath, an HTTP-style httpStatusCode, and the answer inside responseBody. Get this shape wrong and the agent can't read your result.

What the output means: Nothing prints to a screen — a Lambda hands its return value straight back to Bedrock. For a status request the agent receives {"phase": "CrashLoopBackOff", "restarts": 7} wrapped in that response envelope, and folds it into its reasoning.

Try this: Trace what happens if the agent asks for /pods/logs instead: api_path won't match "/pods/status", so result becomes the error dict. Add a second elif api_path == "/pods/logs": branch to handle it.

Create and wire the agent intermediate

Lab W5.2
create_agent.pyimport boto3
agent = boto3.client("bedrock-agent", region_name="us-east-1")

a = agent.create_agent(
    agentName="sre-assistant",
    foundationModel="anthropic.claude-3-5-sonnet-20241022-v2:0",
    instruction="You are an SRE assistant. Diagnose with read-only tools first; "
                "never take a destructive action without explicit confirmation.",
    agentResourceRoleArn="arn:aws:iam::123456789012:role/bedrock-agent-role",
)
agent_id = a["agent"]["agentId"]

# attach the Lambda action group (schema omitted for brevity)
agent.create_agent_action_group(
    agentId=agent_id, agentVersion="DRAFT", actionGroupName="k8s-tools",
    actionGroupExecutor={"lambda": "arn:aws:lambda:us-east-1:123456789012:function:k8s-tools"},
    apiSchema={"s3": {"s3BucketName": "my-schemas", "s3ObjectKey": "k8s.json"}},
)
agent.prepare_agent(agentId=agent_id)   # compile the DRAFT so it is invocable
▶ How this works

This script creates the agent itself from your laptop using boto3 (the AWS SDK for Python). Three things happen in order: make the agent, give it a tool (action group), then prepare it so it's ready to answer. This is set-up you run once, not every time a user chats.

  1. agent = boto3.client("bedrock-agent", region_name="us-east-1") opens a connection to the Bedrock control plane — the API for building agents (a different client than the one that runs them, which you'll see in the next lab).
  2. agent.create_agent(...) makes the agent. The key inputs: foundationModel picks the brain (a Claude model), instruction is the system prompt telling it how to behave ("diagnose read-only first, confirm before destructive actions"), and agentResourceRoleArn is the IAM role granting it permission to call your Lambdas.
  3. agent_id = a["agent"]["agentId"] pulls the new agent's ID out of the response so the next call can attach a tool to it.
  4. agent.create_agent_action_group(...) gives the agent a tool. actionGroupExecutor={"lambda": "..."} says "run this Lambda when the tool fires" (the Lambda from W5.1), and apiSchema points to an OpenAPI schema in S3 that describes the tool's inputs so the model knows how to call it. It attaches to agentVersion="DRAFT" — the editable working copy.
  5. agent.prepare_agent(agentId=agent_id) compiles that DRAFT into a runnable version. Until you prepare it, the agent exists but can't be invoked.

What the output means: No visible output, but afterwards AWS holds a prepared agent with one Lambda-backed tool, ready to receive questions. The ARNs and account number 123456789012 here are placeholders — swap in your own.

Try this: Read the instruction string closely: that plain-English sentence is the single biggest lever on agent behaviour. Rewrite it to also say "always cite the runbook you used" and notice you've changed the agent without touching any tool code.

Invoke and trace advanced

Lab W5.3
invoke_agent.pyimport boto3
rt = boto3.client("bedrock-agent-runtime", region_name="us-east-1")

resp = rt.invoke_agent(
    agentId="AGENT123", agentAliasId="TSTALIASID",
    sessionId="session-1",                       # session state persists across turns
    inputText="Is the checkout-api pod healthy? If not, explain why.",
)
# the response is an event stream
for event in resp["completion"]:
    if "chunk" in event:
        print(event["chunk"]["bytes"].decode(), end="")
▶ How this works

Now the agent is built, this is how you actually talk to it. You send one question and read the answer back. Crucially, the answer doesn't arrive all at once — it streams in pieces, so the loop at the bottom stitches those pieces together as they come.

  1. rt = boto3.client("bedrock-agent-runtime", region_name="us-east-1") opens the runtime client — the one for using a live agent (note it's bedrock-agent-runtime, not the bedrock-agent build client from the last lab).
  2. resp = rt.invoke_agent(...) sends the turn. agentId + agentAliasId pick which agent/version to talk to, and inputText is the user's actual question.
  3. sessionId="session-1" is the memory key. Reuse the same sessionId on the next call and the agent remembers the earlier turns — that's the "session state" the agent keeps for you, so you don't have to resend the whole history.
  4. for event in resp["completion"]: loops over the stream of events coming back. The reply is delivered as a sequence of chunks, not one string.
  5. if "chunk" in event: keeps only the text chunks, and print(event["chunk"]["bytes"].decode(), end="") turns each chunk's raw bytes into text and prints it with no line break, so the sentence builds up smoothly on one line.

What the output means: You'll see the agent's answer appear piece by piece, e.g. a diagnosis of the checkout-api pod. Behind the scenes the agent may have called your Lambda and read the Knowledge Base first — but you only see the final streamed words.

Try this: Call invoke_agent a second time with the same sessionId and a follow-up like "and what caused it?" — the agent uses the earlier turn as context. Change the sessionId to a new value and it starts fresh with no memory.

Exercise W5.1 — Diagnose-then-fix agent

Context: The payoff of an Agent is watching it orchestrate real tools against real knowledge. An SRE-style diagnose-then-fix bot exercises every piece at once: action-group tools, a Knowledge Base of runbooks, and a human-confirmation gate before a destructive action.

Your task: Give your agent two action-group tools (get_pod_status, restart_pod) and a Knowledge Base of runbooks, ask it to fix a crash-looping pod, and trace the orchestration end to end.

Requirements:

  • Two tools attached: a read-only get_pod_status and a mutating restart_pod
  • A Knowledge Base of runbooks the agent can retrieve from
  • Trace which tool ran, in what order, and what the KB returned
  • Show where the agent paused for confirmation before the destructive restart
  • Needs AWS creds; capture the streamed trace so the flow is auditable

💡 Hint: Read-before-write: the diagnostic tool should run first and feed the decision, with confirmation gating the restart_pod call.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Create an agentBeginner

Context: A Bedrock Agent is the managed version of the tool loop you hand-rolled in W3: AWS owns the reason-act cycle, and you supply only the model, the instruction, and a role it can assume. Standing one up is the first step of every agent build.

Your task: Use bedrock-agent.create_agent to define an agent with a foundation model, an instruction (its system prompt), and an agentResourceRoleArn, then print the returned agentId.

Requirements:

  • Create a boto3 client for the bedrock-agent control plane in an explicit region
  • Pass agentName, foundationModel, instruction, and agentResourceRoleArn
  • The instruction reads as a system prompt (what the agent does, when to use tools)
  • Print resp["agent"]["agentId"] so later steps can reference it
  • Note this needs real AWS credentials to run

💡 Hint: The control-plane client (bedrock-agent) creates the agent; a different runtime client will later invoke it. Keep the returned id.

Show solution

The agent is the managed W3 loop; you give it a model, an instruction (its system prompt), and an execution role.

import boto3

agent = boto3.client("bedrock-agent", region_name="us-east-1")
resp = agent.create_agent(
    agentName="support-agent",
    foundationModel="anthropic.claude-3-5-sonnet-20241022-v2:0",
    instruction="You help customers with orders. Use tools for lookups.",
    agentResourceRoleArn="arn:aws:iam::123:role/agent-role",
)
print(resp["agent"]["agentId"])
Exercise 2 · Attach a Lambda-backed action groupIntermediate

Context: An action group is how an Agent gets tools: the Lambda is the code that runs, and an OpenAPI schema is the contract the model reads to know what arguments a tool takes. New tools are always attached to the mutable DRAFT version.

Your task: Call create_agent_action_group to attach an orders tool whose actionGroupExecutor is a Lambda ARN and whose apiSchema is an S3-hosted OpenAPI file, on agentVersion=DRAFT.

Requirements:

  • Target the DRAFT version, not a numbered/published one
  • actionGroupExecutor points at a Lambda function ARN
  • apiSchema references an S3 bucket + object key holding the API contract
  • Give the action group a clear actionGroupName (e.g. orders)
  • Confirm the call returns without error before moving on

💡 Hint: Think executor = the code, schema = the contract. Both hang off the same DRAFT agent until you prepare it.

Show solution

An action group is a tool: actionGroupExecutor.lambda is the code, apiSchema.s3 is the contract, all on the DRAFT version.

import boto3

agent = boto3.client("bedrock-agent", region_name="us-east-1")
agent.create_agent_action_group(
    agentId="AGENT123",
    agentVersion="DRAFT",
    actionGroupName="orders",
    actionGroupExecutor={"lambda": "arn:aws:lambda:us-east-1:123:function:orders"},
    apiSchema={"s3": {"s3BucketName": "my-schemas",
                       "s3ObjectKey": "orders-openapi.json"}},
)
print("action group attached")
Exercise 3 · Prepare and invokeAdvanced

Context: Editing an agent changes only DRAFT; nothing is runnable until you compile it. prepare_agent turns DRAFT into a runnable version, after which the runtime streams the completion back event by event.

Your task: Call prepare_agent on the agent, then use a bedrock-agent-runtime client to invoke_agent with a sessionId and inputText, reading the streamed completion.

Requirements:

  • prepare_agent is called before any invoke
  • Use the bedrock-agent-runtime client (not the control plane) to invoke
  • Pass agentId, agentAliasId, sessionId, and inputText
  • Iterate resp["completion"] and decode each chunk["bytes"] as it arrives
  • Print the assembled text of the streamed answer

💡 Hint: The completion is an event stream, not a single string — loop over it and only act on events that carry a chunk.

Show solution

prepare_agent turns DRAFT into a runnable version; invoke_agent returns an event stream in resp["completion"].

import boto3

agent = boto3.client("bedrock-agent", region_name="us-east-1")
agent.prepare_agent(agentId="AGENT123")

rt = boto3.client("bedrock-agent-runtime", region_name="us-east-1")
resp = rt.invoke_agent(
    agentId="AGENT123", agentAliasId="ALIAS1",
    sessionId="sess-1", inputText="Where is order A100?",
)
for event in resp["completion"]:
    if "chunk" in event:
        print(event["chunk"]["bytes"].decode(), end="")
Exercise 4 · Implement the Lambda action contractExpert

Context: When the Agent decides to call a tool, it invokes your Lambda with a fixed event shape and expects an equally fixed response envelope back. Getting that envelope exactly right is what makes the orchestration work — and it is pure logic you can test offline.

Your task: Write the lambda_handler for the orders action group: read apiPath and the parameters list, and return the required messageVersion/response envelope.

Requirements:

  • Flatten the parameters list into a name→value dict
  • Branch on event["apiPath"] (e.g. /orders/status)
  • Echo back actionGroup and apiPath from the event
  • Return messageVersion, an httpStatusCode, and a responseBody under application/json
  • Prove it offline: feed a hand-made event and assert the status code is 200

💡 Hint: The response must mirror the request's actionGroup and apiPath; the actual data goes inside responseBody["application/json"]["body"].

Show solution

The Lambda receives actionGroup, apiPath, and a parameters list, and must echo them in a specific envelope.

def lambda_handler(event, context):
    params = {p["name"]: p["value"] for p in event.get("parameters", [])}
    if event["apiPath"] == "/orders/status":
        body = {"status": "shipped", "order_id": params.get("order_id")}
    else:
        body = {"error": "unknown path"}
    return {
        "messageVersion": "1.0",
        "response": {
            "actionGroup": event["actionGroup"],
            "apiPath": event["apiPath"],
            "httpStatusCode": 200,
            "responseBody": {"application/json": {"body": str(body)}},
        },
    }

evt = {"actionGroup":"orders","apiPath":"/orders/status",
       "parameters":[{"name":"order_id","value":"A100"}]}
print(lambda_handler(evt, None)["response"]["httpStatusCode"])   # 200
Exercise 5 · Multi-turn session stateProfessional

Context: A coherent multi-turn conversation depends on the Agent remembering earlier turns, and AWS keys that memory off the sessionId you pass. Get the id policy wrong and either users leak into each other's sessions or every turn starts cold.

Your task: Show that reusing one sessionId across two invoke_agent calls preserves context, and model a session-id policy with a session_for(user_id, sessions) helper that hands each user a stable id.

Requirements:

  • session_for returns the same id for repeat calls with the same user_id
  • Different users get different session ids
  • Assert same-user calls compare equal (a durable per-user session)
  • Explain that reusing the id across turns is what makes the agent remember
  • The id logic runs offline; the actual invokes need AWS creds

💡 Hint: A dict.setdefault keyed by user gives you one durable id per user in a couple of lines — no need to regenerate on every turn.

Show solution

AWS manages session memory keyed by sessionId; a stable per-user id keeps a conversation coherent.

import uuid

def session_for(user_id, sessions):
    # one durable session per user; reuse across turns
    return sessions.setdefault(user_id, str(uuid.uuid4()))

sessions = {}
s1 = session_for("u-42", sessions)
s2 = session_for("u-42", sessions)   # same user -> same session
print(s1 == s2)                       # True -> agent remembers prior turns
# rt.invoke_agent(agentId=.., agentAliasId=.., sessionId=s1, inputText="...")
# rt.invoke_agent(agentId=.., agentAliasId=.., sessionId=s1, inputText="and then?")
Exercise 6 · Agent vs hand-rolled tool loop decisionIndustry scenario

Context: Not every assistant should be a managed Agent. A managed Agent removes the loop/memory boilerplate, but a hand-rolled Converse loop is the right call when you need bespoke orchestration the Agent hides. Teams get this trade-off wrong in both directions.

Your task: Encode agent_vs_loop(want_managed_memory, need_custom_orchestration, few_tools) that returns which approach to use, and justify the pick for a support bot with 3 tools and standard memory.

Requirements:

  • Custom orchestration need forces the hand-rolled loop, whatever else is true
  • Managed memory + few tools + no exotic control flow → a Bedrock Agent
  • Return a clear string label (e.g. bedrock-agent vs hand-rolled-loop)
  • Demonstrate the support-bot case resolves to the managed Agent
  • State in one line why the managed loop + session state fits that case

💡 Hint: Check the disqualifier first: if custom orchestration is required, decide immediately; only then weigh memory and tool count.

Show solution

A managed Agent removes loop/memory boilerplate; a hand-rolled loop wins only when you need bespoke orchestration.

def agent_vs_loop(want_managed_memory, need_custom_orchestration, few_tools):
    if need_custom_orchestration:
        return "hand-rolled-loop"   # you need control the Agent hides
    if want_managed_memory and few_tools:
        return "bedrock-agent"      # AWS owns loop + session state
    return "bedrock-agent"

print(agent_vs_loop(True, False, True))   # bedrock-agent
# Support bot: standard memory + 3 tools + no exotic control flow
# -> Bedrock Agent; the managed W3 loop + session state is exactly the fit.

✓ Checkpoint — you can move on when you can…

  • Explain how an Agent maps onto the W3 manual tool loop.
  • Define an action group and the Lambda contract it expects.
  • Attach a Knowledge Base and explain what the agent gains.
  • Invoke an agent with a session and read the streamed completion.

Knowledge check check yourself

✓ Knowledge check

How does a Bedrock Agent relate to the manual W3 tool loop, and what does it manage for you?

Show answer
The agent runs the same call-model -> tool_use -> run tool -> return result -> repeat loop, but AWS orchestrates it: you declare action groups (Lambda-backed tools) and optionally attach a Knowledge Base, and Bedrock calls your Lambdas and keeps session state across turns.
✓ Knowledge check

What is the role of prepare_agent, and how does sessionId affect an invocation?

Show answer
prepare_agent compiles the editable DRAFT into a runnable version; until you prepare it the agent exists but cannot be invoked. Reusing the same sessionId makes the agent remember prior turns (session state); a new sessionId starts a fresh conversation with no memory.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in