Skip to content

AWS Lambda Powertools for .NET - Bedrock Agent Function Resolver

Overview

The Bedrock Agent Function Resolver is a utility for AWS Lambda that simplifies building serverless applications working with Amazon Bedrock Agents. This library eliminates boilerplate code typically required when implementing Lambda functions that serve as action groups for Bedrock Agents.

Amazon Bedrock Agents can invoke functions to perform tasks based on user input. This library provides an elegant way to register, manage, and execute these functions with minimal code, handling all the parameter extraction and response formatting automatically.

Create Amazon Bedrock Agents and focus on building your agent's logic without worrying about parsing and routing requests.

flowchart LR
    Bedrock[LLM] <-- uses --> Agent
    You[User input] --> Agent
    Agent[Bedrock Agent] <-- tool use --> Lambda
    subgraph Agent[Bedrock Agent]
        ToolDescriptions[Tool Definitions]
    end
    subgraph Lambda[Lambda Function]
        direction TB
        Parsing[Parameter Parsing] --> Routing
        Routing --> Code[Your code]
        Code --> ResponseBuilding[Response Building]
    end
    style You stroke:#0F0,stroke-width:2px

Features

  • Easily expose tools for your Large Language Model (LLM) agents
  • Automatic routing based on tool name and function details
  • Graceful error handling and response formatting
  • Fully compatible with .NET 8 AOT compilation through source generation

Terminology

Event handler is a Powertools for AWS feature that processes an event, runs data parsing and validation, routes the request to a specific function, and returns a response to the caller in the proper format.

Function details consist of a list of parameters, defined by their name, data type, and whether they are required. The agent uses these configurations to determine what information it needs to elicit from the user.

Action group is a collection of two resources where you define the actions that the agent should carry out: an OpenAPI schema to define the APIs that the agent can invoke to carry out its tasks, and a Lambda function to execute those actions.

Large Language Models (LLM) are very large deep learning models that are pre-trained on vast amounts of data, capable of extracting meanings from a sequence of text and understanding the relationship between words and phrases on it.

Amazon Bedrock Agent is an Amazon Bedrock feature to build and deploy conversational agents that can interact with your customers using Large Language Models (LLM) and AWS Lambda functions.

Installation

Install the package via NuGet:

1
dotnet add package AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction

Required resources

You must create an Amazon Bedrock Agent with at least one action group. Each action group can contain up to 5 tools, which in turn need to match the ones defined in your Lambda function. Bedrock must have permission to invoke your Lambda function.

Click to see example SAM template
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
Function:
    Timeout: 30
    MemorySize: 256
    Runtime: dotnet8

Resources:
HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
    Handler: FunctionHandler
    CodeUri: hello_world

AirlineAgentRole:
    Type: AWS::IAM::Role
    Properties:
    RoleName: !Sub '${AWS::StackName}-AirlineAgentRole'
    Description: 'Role for Bedrock Airline agent'
    AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
            Principal:
            Service: bedrock.amazonaws.com
            Action: sts:AssumeRole
    Policies:
        - PolicyName: bedrock
        PolicyDocument:
            Version: '2012-10-17'
            Statement:
            - Effect: Allow
                Action: 'bedrock:*'
                Resource:
                - !Sub 'arn:aws:bedrock:us-*::foundation-model/*'
                - !Sub 'arn:aws:bedrock:us-*:*:inference-profile/*'

BedrockAgentInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
    FunctionName: !Ref HelloWorldFunction
    Action: lambda:InvokeFunction
    Principal: bedrock.amazonaws.com
    SourceAccount: !Ref 'AWS::AccountId'
    SourceArn: !Sub 'arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:agent/${AirlineAgent}'

# Bedrock Agent
AirlineAgent:
    Type: AWS::Bedrock::Agent
    Properties:
    AgentName: AirlineAgent
    Description: 'A simple Airline agent'
    FoundationModel: !Sub 'arn:aws:bedrock:us-west-2:${AWS::AccountId}:inference-profile/us.amazon.nova-pro-v1:0'
    Instruction: |
        You are an airport traffic control agent. You will be given a city name and you will return the airport code for that city.
    AgentResourceRoleArn: !GetAtt AirlineAgentRole.Arn
    AutoPrepare: true
    ActionGroups:
        - ActionGroupName: AirlineActionGroup
        ActionGroupExecutor:
            Lambda: !GetAtt AirlineAgentFunction.Arn
        FunctionSchema:
            Functions:
            - Name: getAirportCodeForCity
                Description: 'Get the airport code for a given city'
                Parameters:
                city:
                    Type: string
                    Description: 'The name of the city to get the airport code for'
                    Required: true

Basic Usage

To create an agent, use the BedrockAgentFunctionResolver to register your tools and handle the requests. The resolver will automatically parse the request, route it to the appropriate function, and return a well-formed response that includes the tool's output and any existing session attributes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using Amazon.Lambda.Core;
using Amazon.Lambda.RuntimeSupport;
using AWS.Lambda.Powertools.EventHandler.Resolvers;
using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models;

var resolver = new BedrockAgentFunctionResolver();

resolver
    .Tool("GetWeather", (string city) => $"The weather in {city} is sunny")
    .Tool("CalculateSum", (int a, int b) => $"The sum of {a} and {b} is {a + b}")
    .Tool("GetCurrentTime", () => $"The current time is {DateTime.Now}");

// The function handler that will be called for each Lambda event
var handler = async (BedrockFunctionRequest input, ILambdaContext context) =>
{
    return await resolver.ResolveAsync(input, context);
};

// Build the Lambda runtime client passing in the handler to call for each
// event and the JSON serializer to use for translating Lambda JSON documents
// to .NET types.
await LambdaBootstrapBuilder.Create(handler, new DefaultLambdaJsonSerializer())
    .Build()
    .RunAsync();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using AWS.Lambda.Powertools.EventHandler.Resolvers;
using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models;
using Amazon.Lambda.Core;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace MyLambdaFunction
{
    public class Function
    {
        private readonly BedrockAgentFunctionResolver _resolver;

        public Function()
        {
            _resolver = new BedrockAgentFunctionResolver();

            // Register simple tool functions
            _resolver
                .Tool("GetWeather", (string city) => $"The weather in {city} is sunny")
                .Tool("CalculateSum", (int a, int b) => $"The sum of {a} and {b} is {a + b}")
                .Tool("GetCurrentTime", () => $"The current time is {DateTime.Now}");
        }

        // Lambda handler function
        public BedrockFunctionResponse FunctionHandler(
            BedrockFunctionRequest input, ILambdaContext context)
        {
            return _resolver.Resolve(input, context);
        }
    }
}

When the Bedrock Agent invokes your Lambda function with a request to use the "GetWeather" tool and a parameter for "city", the resolver automatically extracts the parameter, passes it to your function, and formats the response.

Response Format

You can return any type from your tool function, the library will automatically format the response in a way that Bedrock Agents expect.

The response will include:

  • The action group name
  • The function name
  • The function response body, which can be a text response or other structured data in string format
  • Any session attributes that were passed in the request or modified during the function execution

The response body will always be a string.

If you want to return an object the best practice is to override the ToString() method of your return type to provide a custom string representation, or if you don't override, create an anonymous object return new {} and pass your object, or simply return a string directly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class AirportInfo
{
    public string City { get; set; } = string.Empty;
    public string Code { get; set; } = string.Empty;
    public string Name { get; set; } = string.Empty;

    public override string ToString()
    {
        return $"{Name} ({Code}) in {City}";
    }
}

resolver.Tool("getAirportCodeForCity", "Get airport code and full name for a specific city", (string city, ILambdaContext context) =>
{
    var airportService = new AirportService();
    var airportInfo = airportService.GetAirportInfoForCity(city);
    // Note: Best approach is to override the ToString method in the AirportInfo class
    return airportInfo;
});

//Alternatively, you can return an anonymous object if you dont override ToString()
// return new {
//     airportInfo
// }; 

How It Works with Amazon Bedrock Agents

  1. When a user interacts with a Bedrock Agent, the agent identifies when it needs to call an action to fulfill the user's request.
  2. The agent determines which function to call and what parameters are needed.
  3. Bedrock sends a request to your Lambda function with the function name and parameters.
  4. The BedrockAgentFunctionResolver automatically:
  5. Finds the registered handler for the requested function
  6. Extracts and converts parameters to the correct types
  7. Invokes your handler with the parameters
  8. Formats the response in the way Bedrock Agents expect
  9. The agent receives the response and uses it to continue the conversation with the user

Advanced Usage

Custom type serialization

You can have your own custom types as arguments to the tool function. The library will automatically handle serialization and deserialization of these types. In this case, you need to ensure that your custom type is serializable to JSON, if serialization fails, the object will be null.

1
2
3
4
5
6
7
8
9
resolver.Tool(
    name: "PriceCalculator",
    description: "Calculate total price with tax",
    handler: (MyCustomType myCustomType) =>
    {
        var withTax = myCustomType.Price * 1.2m;
        return $"Total price with tax: {withTax.ToString("F2", CultureInfo.InvariantCulture)}";
    }
);

Custom type serialization native AOT

For native AOT compilation, you can use JsonSerializerContext and pass it to BedrockAgentFunctionResolver. This allows the library to generate the necessary serialization code at compile time, ensuring compatibility with AOT.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var resolver = new BedrockAgentFunctionResolver(MycustomSerializationContext.Default);
resolver.Tool(
    name: "PriceCalculator",
    description: "Calculate total price with tax",
    handler: (MyCustomType myCustomType) =>
    {
        var withTax = myCustomType.Price * 1.2m;
        return $"Total price with tax: {withTax.ToString("F2", CultureInfo.InvariantCulture)}";
    }
);

[JsonSerializable(typeof(MyCustomType))]
public partial class MycustomSerializationContext : JsonSerializerContext
{
}

Accessing Lambda Context

You can access to the original Lambda event or context for additional information. These are passed to the handler function as optional arguments.

1
2
3
4
5
6
7
8
resolver.Tool(
    "LogRequest",
    "Logs request information and returns confirmation",
    (string requestId, ILambdaContext context) => 
    {
        context.Logger.LogLine($"Processing request {requestId}");
        return $"Request {requestId} logged successfully";
    });

Handling errors

By default, we will handle errors gracefully and return a well-formed response to the agent so that it can continue the conversation with the user.

When an error occurs, we send back an error message in the response body that includes the error type and message. The agent will then use this information to let the user know that something went wrong.

If you want to handle errors differently, you can return a BedrockFunctionResponse with a custom Body and ResponseState set to FAILURE. This is useful when you want to abort the conversation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
resolver.Tool("CustomFailure", () => 
{
    // Return a custom FAILURE response
    return new BedrockFunctionResponse
    {
        Response = new Response
        {
            ActionGroup = "TestGroup",
            Function = "CustomFailure",
            FunctionResponse = new FunctionResponse
            {
                ResponseBody = new ResponseBody
                {
                    Text = new TextBody 
                    { 
                        Body = "Critical error occurred: Database unavailable" 
                    }
                },
                ResponseState = ResponseState.FAILURE  // Mark as FAILURE to abort the conversation
            }
        }
    };
});

Setting session attributes

When Bedrock Agents invoke your Lambda function, it can pass session attributes that you can use to store information across multiple interactions with the user. You can access these attributes in your handler function and modify them as needed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Create a counter tool that reads and updates session attributes
resolver.Tool("CounterTool", (BedrockFunctionRequest request) => 
{
    // Read the current count from session attributes
    int currentCount = 0;
    if (request.SessionAttributes != null && 
        request.SessionAttributes.TryGetValue("counter", out var countStr) &&
        int.TryParse(countStr, out var count))
    {
        currentCount = count;
    }

    // Increment the counter
    currentCount++;

    // Create a new dictionary with updated counter
    var updatedSessionAttributes = new Dictionary<string, string>(request.SessionAttributes ?? new Dictionary<string, string>())
    {
        ["counter"] = currentCount.ToString(),
        ["lastAccessed"] = DateTime.UtcNow.ToString("o")
    };

    // Return response with updated session attributes
    return new BedrockFunctionResponse
    {
        Response = new Response
        {
            ActionGroup = request.ActionGroup,
            Function = request.Function,
            FunctionResponse = new FunctionResponse
            {
                ResponseBody = new ResponseBody
                {
                    Text = new TextBody { Body = $"Current count: {currentCount}" }
                }
            }
        },
        SessionAttributes = updatedSessionAttributes,
        PromptSessionAttributes = request.PromptSessionAttributes
    };
});

Asynchronous Functions

Register and use asynchronous functions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
_resolver.Tool(
    "FetchUserData",
    "Fetches user data from external API", 
    async (string userId, ILambdaContext ctx) => 
    {
        // Log the request
        ctx.Logger.LogLine($"Fetching data for user {userId}");

        // Simulate API call
        await Task.Delay(100); 

        // Return user information
        return new { Id = userId, Name = "John Doe", Status = "Active" }.ToString();
    });

Direct Access to Request Payload

Access the raw Bedrock Agent request:

1
2
3
4
5
6
7
8
9
_resolver.Tool(
    "ProcessRawRequest",
    "Processes the raw Bedrock Agent request", 
    (BedrockFunctionRequest input) => 
    {
        var functionName = input.Function;
        var parameterCount = input.Parameters.Count;
        return $"Received request for {functionName} with {parameterCount} parameters";
    });

Dependency Injection

The library supports dependency injection for integrating with services:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
using Microsoft.Extensions.DependencyInjection;

// Set up dependency injection
var services = new ServiceCollection();
services.AddSingleton<IWeatherService, WeatherService>();
services.AddBedrockResolver(); // Extension method to register the resolver

var serviceProvider = services.BuildServiceProvider();
var resolver = serviceProvider.GetRequiredService<BedrockAgentFunctionResolver>();

// Register a tool that uses an injected service
resolver.Tool(
    "GetWeatherForecast",
    "Gets the weather forecast for a location",
    (string city, IWeatherService weatherService, ILambdaContext ctx) => 
    {
        ctx.Logger.LogLine($"Getting weather for {city}");
        return weatherService.GetForecast(city);
    });

Using Attributes to Define Tools

You can define Bedrock Agent functions using attributes instead of explicit registration. This approach provides a clean, declarative way to organize your tools into classes:

Define Tool Classes with Attributes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Define your tool class with BedrockFunctionType attribute
[BedrockFunctionType]
public class WeatherTools
{
    // Each method marked with BedrockFunctionTool attribute becomes a tool
    [BedrockFunctionTool(Name = "GetWeather", Description = "Gets weather forecast for a location")]
    public static string GetWeather(string city, int days)
    {
        return $"Weather forecast for {city} for the next {days} days: Sunny";
    }

    // Supports dependency injection and Lambda context access
    [BedrockFunctionTool(Name = "GetDetailedForecast", Description = "Gets detailed weather forecast")]
    public static string GetDetailedForecast(
        string location, 
        IWeatherService weatherService, 
        ILambdaContext context)
    {
        context.Logger.LogLine($"Getting forecast for {location}");
        return weatherService.GetForecast(location);
    }
}

Register Tool Classes in Your Application

Using the extension method provided in the library, you can easily register all tools from a class:

1
2
3
4
5
6
7
var services = new ServiceCollection();
services.AddSingleton<IWeatherService, WeatherService>();
services.AddBedrockResolver(); // Extension method to register the resolver

var serviceProvider = services.BuildServiceProvider();
var resolver = serviceProvider.GetRequiredService<BedrockAgentFunctionResolver>()
    .RegisterTool<WeatherTools>(); // Register tools from the class during service registration

Complete Example with Dependency Injection

You can find examples in the Powertools for AWS Lambda (.NET) GitHub repository.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
using Amazon.BedrockAgentRuntime.Model;
using Amazon.Lambda.Core;
using AWS.Lambda.Powertools.EventHandler;
using Microsoft.Extensions.DependencyInjection;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace MyBedrockAgent
{
    // Service interfaces and implementations
    public interface IWeatherService
    {
        string GetForecast(string city);
    }

    public class WeatherService : IWeatherService
    {
        public string GetForecast(string city) => $"Weather forecast for {city}: Sunny, 75°F";
    }

    public interface IProductService
    {
        string CheckInventory(string productId);
    }

    public class ProductService : IProductService
    {
        public string CheckInventory(string productId) => $"Product {productId} has 25 units in stock";
    }

    // Main Lambda function
    public class Function
    {
        private readonly BedrockAgentFunctionResolver _resolver;

        public Function()
        {
            // Set up dependency injection
            var services = new ServiceCollection();
            services.AddSingleton<IWeatherService, WeatherService>();
            services.AddSingleton<IProductService, ProductService>();
            services.AddBedrockResolver(); // Extension method to register the resolver

            var serviceProvider = services.BuildServiceProvider();
            _resolver = serviceProvider.GetRequiredService<BedrockAgentFunctionResolver>();

            // Register tool functions that use injected services
            _resolver
                .Tool("GetWeatherForecast", 
                    "Gets weather forecast for a city",
                    (string city, IWeatherService weatherService, ILambdaContext ctx) => 
                    {
                        ctx.Logger.LogLine($"Weather request for {city}");
                        return weatherService.GetForecast(city);
                    })
                .Tool("CheckInventory",
                    "Checks inventory for a product",
                    (string productId, IProductService productService) => 
                        productService.CheckInventory(productId))
                .Tool("GetServerTime",
                    "Returns the current server time",
                    () => DateTime.Now.ToString("F"));
        }

        public ActionGroupInvocationOutput FunctionHandler(
            ActionGroupInvocationInput input, ILambdaContext context)
        {
            return _resolver.Resolve(input, context);
        }
    }
}