Bedrock Agents as code (Part II)

with generative AI CDK constructs by cdklabs

--

Introduction

A few months ago I wrote an article on getting started with Bedrock Agents using CDK (Cloud Development Kit). Since then, AWS Bedrock and the AWS Bedrock agent construct have been added to the CDK package. This makes it much easier to set up agents, and focus on the actual agent objectives you want to work on. In this article I will show you how to set up a Bedrock Agent to fill in and send a report using CDK in Typescript and Python based on my Bedrock Agents Starter in CDK repository.

An AI agent is a computer program or system that autonomously perceives its environment, processes information, and takes rational actions to achieve specific objectives. In the context of LLMs, an agent uses a large language model to perform complex tasks, engage in conversations, and reason, demonstrating a degree of autonomy akin to that of digital assistants but with enhanced capabilities in written communication and task execution.

Some core questions; Like a human assistant, there are a few important things to consider:

  • What can the assistant see, access, search for?
  • What triggers an agent to perform on the objectives?
  • What actions can the agent take?
  • How does the human in the loop play a role in the agent’s actions?

If you know in great detail what needs to happen and if you have a good insight on all involved conditions, you do not need an Agent, you need a state-machine. Consider agents as more creative state machines that can reason and find alternative approaches, asking humans for extra information or feedback if needed.

In the following, we’ll create a conceptual agent-flow to update construction site managers with the daily progress report. The code is meant as an illustration only, the repository only provides the starter!

The Agent

Let’s build an agent that helps us to fill out the missing fields in a progress report on a construction site; Typically such a report has a list of the sub-contractors on site, a brief description of the work they are doing and the weather conditions on site. When all the data for the report is available, we want to send a personalised message to the site manager.

Conceptual: complete a construction progress report and send a personalised message to the site manager.

One of the most important things to do when building an agent is to define the objectives. This will help you to focus on the actual work that the agent needs to do. In our case, the agent needs to:

  • Fill out the missing fields in the progress report
  • Send a personalised WhatsApp message to the site manager with the highlights of the report

We’ll provide the agent with tools that will help it to achieve these objectives. The actions are quite self-explanatory.

  • get-report.
  • get-weather-information
  • get-site-manager-contact-details
  • get-work-log
  • update-report-field
  • send-message

The Stack

In our CDK stack, we’ll need to define the following:

  • The Bedrock Agent itself
  • The action group(s)
  • The AWS Lambda method that will contain the business logic for the actions the agent is able to perform
  • A datastore to keep some state around the objective of the Agent (AWS DynamoDB)
  • Roles and permissions for the Bedrock Agent and the AWS Lambda method

All code can be found here. (disclaimer; running this code falls outside the free-tier of an AWS account.)

The Agent construct

The Agent construct is not yet part of the CDK package, so you’ll need to install it first. (You can also check out the source code and run npm ci )

npm install @cdklabs/generative-ai-cdk-constructs

Once you have installed the package, you can create a new Bedrock Agent construct in your CDK stack. Here’s an example of how you can do this in typescript:

// Create a new Bedrock Agent construct
const agent = new Agent(this, "Agent", {
// Specify the foundational model to use for the agent)
foundationModel: BedrockFoundationModel.ANTHROPIC_CLAUDE_SONNET_V1_0,

// Instruction prompt
instruction: `
You're a helpful assistant to the site manager or a construction site; Given a report_id;There will be an indication of which fields are missing from the report and you'll use actions to fill out all missing ones (do not add other values.).

Once all the fields are filled out, you will send the report to the correct site manager. Before doing so, ensure you check the site manager’s preferences to personalize the message accordingly. The message will be send to a mobile device. The message should contain all relevant information from the report. (so the log, temperature, location); It should be clear and concise and complete.
`,
enableUserInput: false,
shouldPrepareAgent: true,
});

The instruction prompt is a brief description of what the agent is supposed to do. The enableUserInput property is set to false, which means that the agent will not require any user input to perform its tasks. The shouldPrepareAgent property is set to true, which means that the agent will be able to prepare itself to perform its tasks as part of the CDK deployment.

The Lambda function

We’ll be using the PythonFunction construct to define the lambda function that will contain the logic for the actions that the agent can perform. This construct is not part of the CDK package.

npm install @aws-cdk/aws-lambda-python-alpha

With this construct, you can define the lambda function as follows:

const lambdaRole = new Role(this, "LambdaRole", {
assumedBy: new ServicePrincipal("lambda.amazonaws.com"),
managedPolicies: [
ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AWSLambdaBasicExecutionRole",
),
],
});

const actionGroupLambda = new PythonFunction(this, "dbLambda", {
handler: "lambda_handler",
entry: "lambda/actiongroup",
index: "main.py",
runtime: Runtime.PYTHON_3_11,
role: lambdaRole,
timeout: cdk.Duration.seconds(20),
environment: {},
});

Actions logic

The logic for the actions that the agent can perform is defined in a lambda function.

Within the lambda function, the lambda_handler function is the entry point for the lambda function. A Bedrock agent event is passed to the function, which contains the action that the agent wants to perform as well as additional information passed down from the agent invocation (e.g. data that can be used to create authorized sessions).

A first match-case block is used to determine the action group that the agent wants to perform. A second match-case block is used to determine the specific function that the agent wants to perform. The function is then executed based on the action group and function that the agent wants to perform.

def lambda_handler(event, context):

# Get the agent, actionGroup and function from the event
agent = event["agent"]
actionGroup = event["actionGroup"]
function = event["function"]
logger.info(f"Invoked by agent: {agent}")
logger.info(f"Received event: {pformat(event)}")

body = ""

match actionGroup:
case "report-complete-actions":
# Get the parameters from the event
params = event.get("params", [])
params_dict = convert_params_to_dict(params)
logger.info(f"Received params: {pformat(params_dict)}")

# Perform the action
match function:
case "get-report":
logger.info("Getting report...")
# Get report code here
case "get-weather-information":
logger.info("Getting weather information...")
# Get weather information code here
case "get-site-manager-contact-details":
logger.info("Getting site manager contact details...")
# Get site manager contact details code here
case "get-work-log":
logger.info("Getting work log...")
# Get work log code here
case "update-report-field":
logger.info("Updating report field...")
# Update report field code here
case "send-message":
logger.info("Sending message...")
# Send message code here
case _:
logger.warning(f"Function '{function}' not supported")
case _:
logger.warning(f"Action group '{actionGroup}' not supported")

# Create the action response
response = {
"actionGroup": actionGroup,
"function": function,
"functionResponse": {"responseBody": {"TEXT": {"body": body}}},
}

function_response = {'response': response, 'messageVersion': event['messageVersion']}

logger.info(f"sending response: {pformat(function_response)}")

return function_response

Invoking the agent

Our agent is now define and active, our agents can be triggered via the AWS Console, the AWS CLI or SDK. Via the Console, your can enter the inputText on the left; If the Trace is enabled you’ll be able to follow the steps of the agent on the right.

Trace steps of the Agent.

If you are working in a Python environment, the bedrock-agent-runtime client is the easiest way to interact with the agent. The completion object will contain all reasoning and action steps.

agent_client = boto3.client('bedrock-agent-runtime', region_name='eu-central-1')

agent_id = '<AGENT_ID>'
agent_alias_id = '<AGENT_ALIAS_ID>'

response = agent_client.invoke_agent(
agentId=agent_id,
agentAliasId=agent_alias_id,
enableTrace=True,
endSession=False,
sessionId=str(uuid.uuid4()),
inputText=input_text
)

completion = response['completion']

Let’s look at a detailed flow. At the start of the invocation of the agent, the report looks like this in the database.

Report before the Agent runs.

The humidity is filled in; but that is not temperature information, nor information on the actual works that took place. Let’s see our Agent complete this.

RATIONALE First, I need to get the report details by calling the get-report function with the provided report_id.
INVOCATION FUNCTION get-report
OBSERVATION
report_id: 123
manager_id: 666
project_id: 42
location: Ghent
temperature: Missing
humidity: 78%
work_log: Missing
RATIONALE The report is missing the temperature and work_log fields. I will need to fill those in before sending the report to the site manager.
INVOCATION FUNCTION get-weather-information
OBSERVATION
temperature: 16.83°C
humidity: 78%
RATIONALE I now have the temperature information for the location Ghent. I will update the report with this temperature value.
INVOCATION FUNCTION update-report-field
OBSERVATION
Updated field 'temperature' to '16.83°C'
The work logs in the database
RATIONALE The temperature field is now updated in the report. Next I need to get the work log for the project associated with this report.
INVOCATION FUNCTION get-work-log
OBSERVATION
timestamp: 2024-09-08 10:28:56: Putting the steel reinforcement in place before the concrete works
timestamp: 2024-09-08 11:02:16: concrete works on the 2nd floor
timestamp: 2024-09-08 11:52:16: Started on removing the old roof
RATIONALE I now have the work log for the project. I will update the report with this log.
INVOCATION FUNCTION update-report-field
OBSERVATION
Updated field 'work_log' to
'timestamp: 2024-09-08 10:28:56: Putting the steel reinforcement in place before the concrete works
timestamp: 2024-09-08 11:02:16: concrete works on the 2nd floor
timestamp: 2024-09-08 11:52:16: Started on removing the old roof'
RATIONALE The report now has all required fields filled out. I will get the contact details for the site manager and send them a message with the report details.
INVOCATION FUNCTION get-site-manager-contact-details
OBSERVATION
manager_id: 666
email: pieterjan@example.com
language: Dutch
notes: Pieterjan only speaks Dutch, please take this into account
phone_number: +11111111111
full_name: Pieterjan Criel

For further communication take this information into account.
RATIONALE The site manager prefers communication in Dutch, so I will send the message in Dutch.
INVOCATION FUNCTION send-message
OBSERVATION A message was sent to +11111111111 (manager_id: 666) with SID: xxxxx
Message I got on my phone (In Dutch)

Conclusion

Compared to a few months ago, it is now much easier to set up Bedrock agents using CDK. Since all of these components build on top of existing, serverless services like Lambda and Bedrock, it is easy to scale the agent as needed and you only pay for what you use.

We looked at a conceptual Agent that completes a report and send the highlights of that report to a stakeholder in a personalised manner. Looking at Agent; This is what they should to in my opinion

  • They can be ever on
  • The content the create (combination of reasoning, actions, output) can endlessly iterate
  • Personalise all interactions with humans.

This article serves as an example using the Bedrock Agents in CDK starter repository.

--

--

Pieterjan Criel @pjcr
Product & Engineering at Showpad

👨‍👩‍👧‍👦 Dad of two 🇧🇪 Ghent 🎈 @Balloon_inc / @Aicon_inc 👨‍💻 Coding 🧪 Data science 📈 Graph enthusiast 👨‍💻 Principal Engineer @showpad