From 108c9f0b039853565b5e1367c92c9a640bd57643 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 22 May 2025 12:29:29 +0100 Subject: [PATCH 01/52] bedrock agent function first commit --- libraries/AWS.Lambda.Powertools.sln | 15 ++ ...andler.BedrockAgentFunctionResolver.csproj | 20 ++ .../BedrockAgentFunctionResolver.cs | 104 ++++++++ .../InternalsVisibleTo.cs | 18 ++ .../Readme.md | 57 +++++ libraries/src/Directory.Packages.props | 1 + ...ambda.Powertools.EventHandler.Tests.csproj | 5 + .../BedrockAgentFunctionResolverTests.cs | 232 ++++++++++++++++++ .../bedrockFunctionEvent.json | 27 ++ 9 files changed, 479 insertions(+) create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/InternalsVisibleTo.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent.json diff --git a/libraries/AWS.Lambda.Powertools.sln b/libraries/AWS.Lambda.Powertools.sln index 5d7cd4f9..0ca62f24 100644 --- a/libraries/AWS.Lambda.Powertools.sln +++ b/libraries/AWS.Lambda.Powertools.sln @@ -109,6 +109,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Event EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.EventHandler", "src\AWS.Lambda.Powertools.EventHandler\AWS.Lambda.Powertools.EventHandler.csproj", "{F4B8D5AF-D3CA-4910-A14D-E5BAEF0FD1DE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver", "src\AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver\AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj", "{281F7EB5-ACE5-458F-BC88-46A8899DF3BA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -590,6 +592,18 @@ Global {F4B8D5AF-D3CA-4910-A14D-E5BAEF0FD1DE}.Release|x64.Build.0 = Release|Any CPU {F4B8D5AF-D3CA-4910-A14D-E5BAEF0FD1DE}.Release|x86.ActiveCfg = Release|Any CPU {F4B8D5AF-D3CA-4910-A14D-E5BAEF0FD1DE}.Release|x86.Build.0 = Release|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Debug|x64.ActiveCfg = Debug|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Debug|x64.Build.0 = Debug|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Debug|x86.ActiveCfg = Debug|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Debug|x86.Build.0 = Debug|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|Any CPU.Build.0 = Release|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|x64.ActiveCfg = Release|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|x64.Build.0 = Release|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|x86.ActiveCfg = Release|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution @@ -641,5 +655,6 @@ Global {7FC6DD65-0352-4139-8D08-B25C0A0403E3} = {4EAB66F9-C9CB-4E8A-BEE6-A14CD7FDE02F} {61374D8E-F77C-4A31-AE07-35DAF1847369} = {1CFF5568-8486-475F-81F6-06105C437528} {F4B8D5AF-D3CA-4910-A14D-E5BAEF0FD1DE} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5} + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5} EndGlobalSection EndGlobal diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj new file mode 100644 index 00000000..a392b395 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj @@ -0,0 +1,20 @@ + + + + + AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver + Powertools for AWS Lambda (.NET) - Event Handler Bedrock Agent Function Resolver package. + AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver + AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver + net8.0 + false + enable + enable + + + + + + + + \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs new file mode 100644 index 00000000..946b363c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs @@ -0,0 +1,104 @@ +using Amazon.BedrockAgentRuntime.Model; +using Amazon.Lambda.Core; + +// ReSharper disable once CheckNamespace +namespace AWS.Lambda.Powertools.EventHandler +{ + /// + /// Resolver for Amazon Bedrock Agent functions. + /// Routes function calls to appropriate handlers based on function name. + /// + public class BedrockAgentFunctionResolver + { + private readonly Dictionary> _handlers = new(); + + /// + /// Registers a handler for a tool function without parameters. + /// + /// The function name to handle + /// Function handler without parameters + /// Optional description of the function + public BedrockAgentFunctionResolver Tool(string name, Func handler, string description = "") + { + _handlers[name] = (_, _) => handler(); + return this; + } + + /// + /// Registers a handler for a tool function with input. + /// + /// The function name to handle + /// Function handler with input + /// Optional description of the function + public BedrockAgentFunctionResolver Tool(string name, Func handler, string description = "") + { + _handlers[name] = (input, _) => handler(input); + return this; + } + + /// + /// Registers a handler for a tool function with input and context. + /// + /// The function name to handle + /// Function handler with input and context + /// Optional description of the function + public BedrockAgentFunctionResolver Tool(string name, Func handler, string description = "") + { + _handlers[name] = (input, context) => handler(input, context ?? throw new ArgumentNullException(nameof(context))); + return this; + } + + /// + /// Resolves and processes a Bedrock Agent function invocation. + /// + /// The function invocation input + /// Lambda execution context + public ActionGroupInvocationOutput Resolve(ActionGroupInvocationInput input, ILambdaContext? context = null) + { + return ResolveAsync(input, context).GetAwaiter().GetResult(); + } + + /// + /// Asynchronously resolves and processes a Bedrock Agent function invocation. + /// + /// The function invocation input + /// Lambda execution context + public async Task ResolveAsync(ActionGroupInvocationInput input, ILambdaContext? context = null) + { + return await Task.FromResult(HandleEvent(input, context)); + } + + private ActionGroupInvocationOutput HandleEvent(ActionGroupInvocationInput input, ILambdaContext? context) + { + if (string.IsNullOrEmpty(input.Function)) + { + return new ActionGroupInvocationOutput + { + Text = "No function specified in the request" + }; + } + + if (_handlers.TryGetValue(input.Function, out var handler)) + { + try + { + return handler(input, context); + } + catch (Exception ex) + { + context?.Logger.LogError($"Error executing function {input.Function}: {ex.Message}"); + return new ActionGroupInvocationOutput + { + Text = $"Error executing function: {ex.Message}" + }; + } + } + + context?.Logger.LogWarning($"No handler registered for function: {input.Function}"); + return new ActionGroupInvocationOutput + { + Text = $"No handler registered for function: {input.Function}" + }; + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/InternalsVisibleTo.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/InternalsVisibleTo.cs new file mode 100644 index 00000000..9e952373 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/InternalsVisibleTo.cs @@ -0,0 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.EventHandler.Tests")] \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md new file mode 100644 index 00000000..08947c02 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md @@ -0,0 +1,57 @@ +# AWS Lambda Powertools for .NET - Bedrock Agent Function Resolver + +## Overview +The Bedrock Agent Function Resolver is a custom function resolver for AWS Lambda Powertools for .NET. It is designed to work with the Bedrock Agent, a tool that simplifies the process of building and deploying serverless applications on AWS Lambda. +The Bedrock Agent Function Resolver allows you to easily resolve and invoke Lambda functions using the Bedrock Agent's conventions and best practices. +This custom function resolver is part of the AWS Lambda Powertools for .NET library, which provides a suite of utilities for building serverless applications on AWS Lambda. +## Features +- Custom function resolver for AWS Lambda Powertools for .NET +- Supports Bedrock Agent conventions and best practices +- Simplifies the process of resolving and invoking Lambda functions +- Integrates with AWS Lambda Powertools for .NET library +- Supports dependency injection and configuration +- Provides a consistent and easy-to-use API for resolving functions +- Supports asynchronous and synchronous function invocation +- Supports error handling and logging +- Supports custom serialization and deserialization +- Supports custom middleware and filters + +## Getting Started +To get started with the Bedrock Agent Function Resolver, you need to install the AWS Lambda Powertools for .NET library and the Bedrock Agent Function Resolver package. You can do this using NuGet: + +```bash +dotnet add package AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver +``` +## Usage +To use the Bedrock Agent Function Resolver, you need to create an instance of the `BedrockAgentFunctionResolver` class and register it with the AWS Lambda Powertools for .NET library. You can do this in your Lambda function's entry point: + +```csharp +using Amazon.Lambda.Core; +using Amazon.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver; + + +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + +namespace MyLambdaFunction +{ + public class Function + { + private readonly BedrockAgentFunctionResolver _functionResolver; + + public Function() + { + // Create an instance of the Bedrock Agent Function Resolver + _functionResolver = new BedrockAgentFunctionResolver(); + } + + public async Task FunctionHandler(ILambdaContext context) + { + // Use the function resolver to resolve and invoke a Lambda function + var result = await _functionResolver.ResolveAndInvokeAsync("MyLambdaFunctionName", new { /* input parameters */ }); + + // Process the result + context.Logger.LogLine($"Result: {result}"); + } + } +} +``` \ No newline at end of file diff --git a/libraries/src/Directory.Packages.props b/libraries/src/Directory.Packages.props index db4a6a7f..f9a4730c 100644 --- a/libraries/src/Directory.Packages.props +++ b/libraries/src/Directory.Packages.props @@ -9,6 +9,7 @@ + diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj index eef47181..aab78861 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj @@ -32,6 +32,7 @@ + @@ -40,6 +41,10 @@ PreserveNewest + + + PreserveNewest + diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs new file mode 100644 index 00000000..a406a15f --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs @@ -0,0 +1,232 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Amazon.BedrockAgentRuntime.Model; +using Amazon.Lambda.TestUtilities; + +namespace AWS.Lambda.Powertools.EventHandler.Tests; + +public class BedrockAgentFunctionResolverTests +{ + private readonly ActionGroupInvocationInput _bedrockEvent; + + public BedrockAgentFunctionResolverTests() + { + _bedrockEvent = JsonSerializer.Deserialize( + File.ReadAllText("bedrockFunctionEvent.json"), + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() } + })!; + } + + [Fact] + public void TestFunctionHandlerWithNoParameters() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + + var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + Assert.Equal("Hello, World!", result.Text); + } + + [Fact] + public async Task TestFunctionHandlerWithNoParametersAsync() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + + var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var context = new TestLambdaContext(); + + // Act + var result = await resolver.ResolveAsync(input, context); + + // Assert + Assert.Equal("Hello, World!", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithDescription() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }, + "This is a test function"); + + var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + Assert.Equal("Hello, World!", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithMultiplTools() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction1", () => new ActionGroupInvocationOutput { Text = "Hello from Function 1!" }); + resolver.Tool("TestFunction2", () => new ActionGroupInvocationOutput { Text = "Hello from Function 2!" }); + + var input1 = new ActionGroupInvocationInput { Function = "TestFunction1" }; + var input2 = new ActionGroupInvocationInput { Function = "TestFunction2" }; + var context = new TestLambdaContext(); + + // Act + var result1 = resolver.Resolve(input1, context); + var result2 = resolver.Resolve(input2, context); + + // Assert + Assert.Equal("Hello from Function 1!", result1.Text); + Assert.Equal("Hello from Function 2!", result2.Text); + } + + + [Fact] + public void TestFunctionHandlerWithInput() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction", + (input) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); + + var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + Assert.Equal("Hello, TestFunction!", result.Text); + } + + [Fact] + public async Task TestFunctionHandlerWithInputAsync() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction", + (input) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); + + var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var context = new TestLambdaContext(); + + // Act + var result = await resolver.ResolveAsync(input, context); + + // Assert + Assert.Equal("Hello, TestFunction!", result.Text); + } + + [Fact] + public void TestFunctionHandlerNoToolMatch() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + + var input = new ActionGroupInvocationInput { Function = "NonExistentFunction" }; + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + Assert.Equal("No handler registered for function: NonExistentFunction", result.Text); + } + + [Fact] + public async Task TestFunctionHandlerNoToolMatchAsync() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + + var input = new ActionGroupInvocationInput { Function = "NonExistentFunction" }; + var context = new TestLambdaContext(); + + // Act + var result = await resolver.ResolveAsync(input, context); + + // Assert + Assert.Equal("No handler registered for function: NonExistentFunction", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithParameters() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + + var input = new ActionGroupInvocationInput + { + Function = "TestFunction", + RequestBody = new RequestBody + { + + }, + Parameters = new List + { + new Parameter + { + Name = "a", + Value = "1", + Type = "Number" + }, + new Parameter + { + Name = "b", + Value = "1", + Type = "Number" + } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + Assert.Equal("Hello, World!", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithEvent() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("sum_numbers", (payload, context ) => + { + + return new ActionGroupInvocationOutput { Text = "2" }; + }); + + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(_bedrockEvent, context); + + // Assert + Assert.Equal("2", result.Text); + } +} + +// Types +// String +// Number +// Integer +// Boolean +// Array \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent.json b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent.json new file mode 100644 index 00000000..f2cedeb1 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent.json @@ -0,0 +1,27 @@ +{ + "messageVersion": "1.0", + "function": "sum_numbers", + "sessionId": "455081292773641", + "agent": { + "name": "powertools-test", + "version": "DRAFT", + "id": "WPMRGAPAPJ", + "alias": "TSTALIASID" + }, + "parameters": [ + { + "name": "a", + "type": "number", + "value": "1" + }, + { + "name": "b", + "type": "number", + "value": "1" + } + ], + "actionGroup": "utility-tasks", + "sessionAttributes": {}, + "promptSessionAttributes": {}, + "inputText": "Sum 1 and 1" +} \ No newline at end of file From 64d3a618ebc0434f60b8a89038623014658d541a Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Sun, 25 May 2025 16:18:00 +0100 Subject: [PATCH 02/52] add methods that take DI services, match parameters with handler arguments --- ...andler.BedrockAgentFunctionResolver.csproj | 2 + .../BedrockAgentFunctionResolver.cs | 301 ++++++++++++++++-- .../BedrockAgentFunctionResolverExtensions.cs | 27 ++ .../ParameterAccessor.cs | 126 ++++++++ libraries/src/Directory.Packages.props | 1 + .../BedrockAgentFunctionResolverTests.cs | 170 +++++++++- 6 files changed, 589 insertions(+), 38 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/ParameterAccessor.cs diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj index a392b395..87033029 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj @@ -10,11 +10,13 @@ false enable enable + true + \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs index 946b363c..5f33caa9 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs @@ -1,58 +1,254 @@ -using Amazon.BedrockAgentRuntime.Model; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; -// ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler { - /// - /// Resolver for Amazon Bedrock Agent functions. - /// Routes function calls to appropriate handlers based on function name. - /// public class BedrockAgentFunctionResolver { - private readonly Dictionary> _handlers = new(); + private readonly + Dictionary> + _handlers = new(); + + private static readonly HashSet _bedrockParameterTypes = new() + { + typeof(string), + typeof(int), + typeof(long), + typeof(double), + typeof(bool), + typeof(decimal), + typeof(DateTime), + typeof(Guid) + }; + + private static bool IsBedrockParameter(Type type) => + _bedrockParameterTypes.Contains(type) || type.IsEnum; /// - /// Registers a handler for a tool function without parameters. + /// Registers a handler that directly accepts ActionGroupInvocationInput and returns ActionGroupInvocationOutput /// - /// The function name to handle - /// Function handler without parameters - /// Optional description of the function - public BedrockAgentFunctionResolver Tool(string name, Func handler, string description = "") + public BedrockAgentFunctionResolver Tool( + string name, + Func handler, + string description = "") { - _handlers[name] = (_, _) => handler(); + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + _handlers[name] = handler; return this; } /// - /// Registers a handler for a tool function with input. + /// Registers a handler that directly accepts ActionGroupInvocationInput and returns ActionGroupInvocationOutput /// - /// The function name to handle - /// Function handler with input - /// Optional description of the function - public BedrockAgentFunctionResolver Tool(string name, Func handler, string description = "") + public BedrockAgentFunctionResolver Tool( + string name, + Func handler, + string description = "") { + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + _handlers[name] = (input, _) => handler(input); return this; } /// - /// Registers a handler for a tool function with input and context. + /// Registers a handler for a tool function with automatically converted return type. + /// + public BedrockAgentFunctionResolver Tool( + string name, + string description = "", + Delegate? handler = null) + { + // Delegate to the generic version with object as return type + return Tool(name, description, handler); + } + + /// + /// Registers a handler for a tool function with typed return value. /// - /// The function name to handle - /// Function handler with input and context - /// Optional description of the function - public BedrockAgentFunctionResolver Tool(string name, Func handler, string description = "") + public BedrockAgentFunctionResolver Tool( + string name, + string description = "", + Delegate? handler = null) { - _handlers[name] = (input, context) => handler(input, context ?? throw new ArgumentNullException(nameof(context))); + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + _handlers[name] = (input, context) => + { + var accessor = new ParameterAccessor(input.Parameters); + var parameters = handler.Method.GetParameters(); + var args = new object?[parameters.Length]; + var bedrockParamIndex = 0; + + // Get service provider from resolver if available + var serviceProvider = (this as DIBedrockAgentFunctionResolver)?.ServiceProvider; + + // Map parameters from Bedrock input and DI + for (var i = 0; i < parameters.Length; i++) + { + var parameter = parameters[i]; + var paramType = parameter.ParameterType; + + if (paramType == typeof(ILambdaContext)) + { + args[i] = context; + } + else if (paramType == typeof(ActionGroupInvocationInput)) + { + args[i] = input; + } + else if (IsBedrockParameter(paramType)) + { + var paramName = parameter.Name ?? $"arg{bedrockParamIndex}"; + + // AOT-compatible parameter access - direct type checks + if (paramType == typeof(string)) + args[i] = accessor.Get(paramName); + else if (paramType == typeof(int)) + args[i] = accessor.Get(paramName); + else if (paramType == typeof(long)) + args[i] = accessor.Get(paramName); + else if (paramType == typeof(double)) + args[i] = accessor.Get(paramName); + else if (paramType == typeof(bool)) + args[i] = accessor.Get(paramName); + else if (paramType == typeof(decimal)) + args[i] = accessor.Get(paramName); + else if (paramType == typeof(DateTime)) + args[i] = accessor.Get(paramName); + else if (paramType == typeof(Guid)) + args[i] = accessor.Get(paramName); + else if (paramType.IsEnum) + { + // For enums, get as string and parse + var strValue = accessor.Get(paramName); + args[i] = !string.IsNullOrEmpty(strValue) ? Enum.Parse(paramType, strValue) : null; + } + + bedrockParamIndex++; + } + else if (serviceProvider != null) + { + // Resolve from DI + args[i] = serviceProvider.GetService(paramType); + } + } + + try + { + // Execute the handler + var result = handler.DynamicInvoke(args); + + // Direct return for ActionGroupInvocationOutput + if (result is ActionGroupInvocationOutput output) + return output; + + // Handle async results with specific type checks (AOT-compatible) + if (result is Task outputTask) + return outputTask.Result; + if (result is Task stringTask) + return ConvertToOutput((TResult)(object)stringTask.Result); + if (result is Task intTask) + return ConvertToOutput((TResult)(object)intTask.Result); + if (result is Task boolTask) + return ConvertToOutput((TResult)(object)boolTask.Result); + if (result is Task doubleTask) + return ConvertToOutput((TResult)(object)doubleTask.Result); + if (result is Task longTask) + return ConvertToOutput((TResult)(object)longTask.Result); + if (result is Task decimalTask) + return ConvertToOutput((TResult)(object)decimalTask.Result); + if (result is Task dateTimeTask) + return ConvertToOutput((TResult)(object)dateTimeTask.Result); + if (result is Task guidTask) + return ConvertToOutput((TResult)(object)guidTask.Result); + if (result is Task objectTask) + return ConvertToOutput((TResult)objectTask.Result!); + + // For regular Task with no result + if (result is Task task) + { + task.GetAwaiter().GetResult(); + return new ActionGroupInvocationOutput { Text = string.Empty }; + } + + return ConvertToOutput((TResult)result!); + } + catch (Exception ex) + { + context?.Logger.LogError($"Error executing function {name}: {ex.Message}"); + return new ActionGroupInvocationOutput + { + Text = $"Error executing function: {ex.Message}" + }; + } + }; + + return this; + } + + /// + /// Registers a parameter-less handler that returns ActionGroupInvocationOutput + /// + public BedrockAgentFunctionResolver Tool( + string name, + Func handler, + string description = "") + { + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + _handlers[name] = (input, context) => handler(); + return this; + } + + /// + /// Registers a parameter-less handler with automatic string conversion + /// + public BedrockAgentFunctionResolver Tool( + string name, + Func handler, + string description = "") + { + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + _handlers[name] = (input, context) => new ActionGroupInvocationOutput { Text = handler() }; + return this; + } + + /// + /// Registers a parameter-less handler with automatic object conversion + /// + public BedrockAgentFunctionResolver Tool( + string name, + Func handler, + string description = "") + { + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + _handlers[name] = (input, context) => + { + var result = handler(); + return ConvertToOutput(result); + }; return this; } /// /// Resolves and processes a Bedrock Agent function invocation. /// - /// The function invocation input - /// Lambda execution context public ActionGroupInvocationOutput Resolve(ActionGroupInvocationInput input, ILambdaContext? context = null) { return ResolveAsync(input, context).GetAwaiter().GetResult(); @@ -61,9 +257,8 @@ public ActionGroupInvocationOutput Resolve(ActionGroupInvocationInput input, ILa /// /// Asynchronously resolves and processes a Bedrock Agent function invocation. /// - /// The function invocation input - /// Lambda execution context - public async Task ResolveAsync(ActionGroupInvocationInput input, ILambdaContext? context = null) + public async Task ResolveAsync(ActionGroupInvocationInput input, + ILambdaContext? context = null) { return await Task.FromResult(HandleEvent(input, context)); } @@ -100,5 +295,53 @@ private ActionGroupInvocationOutput HandleEvent(ActionGroupInvocationInput input Text = $"No handler registered for function: {input.Function}" }; } + + private ActionGroupInvocationOutput ConvertToOutput(T result) + { + if (result == null) + { + return new ActionGroupInvocationOutput { Text = string.Empty }; + } + + // If result is already an ActionGroupInvocationOutput, return it directly + if (result is ActionGroupInvocationOutput output) + { + return output; + } + + // For primitive types and strings, convert to string + if (result is string str) + { + return new ActionGroupInvocationOutput { Text = str }; + } + + if (result is int intVal) + { + return new ActionGroupInvocationOutput { Text = intVal.ToString(CultureInfo.InvariantCulture) }; + } + + if (result is double doubleVal) + { + return new ActionGroupInvocationOutput { Text = doubleVal.ToString(CultureInfo.InvariantCulture) }; + } + + if (result is bool boolVal) + { + return new ActionGroupInvocationOutput { Text = boolVal.ToString() }; + } + + if (result is long longVal) + { + return new ActionGroupInvocationOutput { Text = longVal.ToString(CultureInfo.InvariantCulture) }; + } + + if (result is decimal decimalVal) + { + return new ActionGroupInvocationOutput { Text = decimalVal.ToString(CultureInfo.InvariantCulture) }; + } + + // For any other type, use ToString() instead of JSON serialization + return new ActionGroupInvocationOutput { Text = result.ToString() ?? string.Empty }; + } } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs new file mode 100644 index 00000000..003f86d2 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace AWS.Lambda.Powertools.EventHandler +{ + // Service provider-aware resolver + public class DIBedrockAgentFunctionResolver : BedrockAgentFunctionResolver + { + public IServiceProvider ServiceProvider { get; } + + public DIBedrockAgentFunctionResolver(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + } + + public static class BedrockResolverExtensions + { + // Extension to register the resolver in DI + public static IServiceCollection AddBedrockResolver(this IServiceCollection services) + { + services.AddSingleton(sp => + new DIBedrockAgentFunctionResolver(sp)); + return services; + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/ParameterAccessor.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/ParameterAccessor.cs new file mode 100644 index 00000000..51217357 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/ParameterAccessor.cs @@ -0,0 +1,126 @@ +using System.Globalization; +using Amazon.BedrockAgentRuntime.Model; + +namespace AWS.Lambda.Powertools.EventHandler; + +/// +/// Provides strongly-typed access to the parameters of an agent function call. +/// +public class ParameterAccessor +{ + private readonly List _parameters; + + internal ParameterAccessor(List? parameters) + { + _parameters = parameters ?? new List(); + } + + /// + /// Gets a parameter value by name with type conversion. + /// + public T Get(string name) + { + var parameter = _parameters.FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)); + if (parameter == null || parameter.Value == null) + { + return default!; + } + + return ConvertParameter(parameter); + } + + /// + /// Gets a parameter value by index with type conversion. + /// + public T GetAt(int index) + { + if (index < 0 || index >= _parameters.Count) + { + return default!; + } + + var parameter = _parameters[index]; + if (parameter.Value == null) + { + return default!; + } + + return ConvertParameter(parameter); + } + + /// + /// Gets a parameter value by name with fallback to a default value. + /// + public T GetOrDefault(string name, T defaultValue) + { + var parameter = _parameters.FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)); + if (parameter == null || parameter.Value == null) + { + return defaultValue; + } + + return ConvertParameter(parameter); + } + + private static T ConvertParameter(Parameter? parameter) + { + if (parameter == null || parameter.Value == null) + { + return default!; + } + + // Handle different types explicitly for AOT compatibility + if (typeof(T) == typeof(string)) + { + return (T)(object)parameter.Value; + } + + if (typeof(T) == typeof(int) || typeof(T) == typeof(int?)) + { + if (int.TryParse(parameter.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out int result)) + { + return (T)(object)result; + } + return default!; + } + + if (typeof(T) == typeof(double) || typeof(T) == typeof(double?)) + { + if (double.TryParse(parameter.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out double result)) + { + return (T)(object)result; + } + return default!; + } + + if (typeof(T) == typeof(bool) || typeof(T) == typeof(bool?)) + { + if (bool.TryParse(parameter.Value, out bool result)) + { + return (T)(object)result; + } + return default!; + } + + if (typeof(T) == typeof(long) || typeof(T) == typeof(long?)) + { + if (long.TryParse(parameter.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out long result)) + { + return (T)(object)result; + } + return default!; + } + + if (typeof(T) == typeof(decimal) || typeof(T) == typeof(decimal?)) + { + if (decimal.TryParse(parameter.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal result)) + { + return (T)(object)result; + } + return default!; + } + + // Return default for array and complex types + return default!; + } +} \ No newline at end of file diff --git a/libraries/src/Directory.Packages.props b/libraries/src/Directory.Packages.props index f9a4730c..a4421f6f 100644 --- a/libraries/src/Directory.Packages.props +++ b/libraries/src/Directory.Packages.props @@ -14,6 +14,7 @@ + diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs index a406a15f..e30dcd9b 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs @@ -1,7 +1,9 @@ using System.Text.Json; using System.Text.Json.Serialization; using Amazon.BedrockAgentRuntime.Model; +using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; +using Microsoft.Extensions.DependencyInjection; namespace AWS.Lambda.Powertools.EventHandler.Tests; @@ -100,7 +102,7 @@ public void TestFunctionHandlerWithInput() // Arrange var resolver = new BedrockAgentFunctionResolver(); resolver.Tool("TestFunction", - (input) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); + (ActionGroupInvocationInput input, ILambdaContext context) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); var input = new ActionGroupInvocationInput { Function = "TestFunction" }; var context = new TestLambdaContext(); @@ -118,7 +120,7 @@ public async Task TestFunctionHandlerWithInputAsync() // Arrange var resolver = new BedrockAgentFunctionResolver(); resolver.Tool("TestFunction", - (input) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); + (ActionGroupInvocationInput input) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); var input = new ActionGroupInvocationInput { Function = "TestFunction" }; var context = new TestLambdaContext(); @@ -208,23 +210,173 @@ public void TestFunctionHandlerWithEvent() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("sum_numbers", (payload, context ) => + resolver.Tool( + name: "GetCustomForecast", + description: "Get detailed forecast for a location", + handler: (string location, int days, ILambdaContext ctx) => { + ctx.Logger.LogLine($"Getting forecast for {location}"); + return $"{days}-day forecast for {location}"; + } + ); + + resolver.Tool( + name: "Greet", + description: "Greet a user", + handler: (string name) => { + return $"Hello {name}"; + } + ); + + resolver.Tool( + name: "Simple", + description: "Greet a user", + handler: () => { + return "Hello"; + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "GetCustomForecast", + Parameters = new List + { + new Parameter + { + Name = "location", + Value = "Lisbon", + Type = "String" + }, + new Parameter + { + Name = "days", + Value = "1", + Type = "Number" + } + } + }; + + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + Assert.Equal("1-day forecast for Lisbon", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithEventAndServices() + { + // Arrange + + // Setup DI + var services = new ServiceCollection(); + services.AddSingleton(new HttpClient()); + services.AddBedrockResolver(); + + var serviceProvider = services.BuildServiceProvider(); + var resolver = serviceProvider.GetRequiredService(); + + resolver.Tool( + name: "GetCustomForecast", + description: "Get detailed forecast for a location", + handler: async (string location, int days, HttpClient client, ILambdaContext ctx) => + { + var resp = await client.GetStringAsync("https://api.open-meteo.com/v1/forecast?latitude=38.7167&longitude=-9.1333¤t=temperature_2m"); + return resp; + } + ); + + resolver.Tool( + name: "Greet", + description: "Greet a user", + handler: (string name) => { + return $"Hello {name}"; + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "GetCustomForecast", + Parameters = new List + { + new Parameter + { + Name = "location", + Value = "Lisbon", + Type = "String" + }, + new Parameter + { + Name = "days", + Value = "1", + Type = "Number" + } + } + }; + + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + Assert.Equal("1-day forecast for Lisbon", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithEventTypes() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "GetCustomForecast", + description: "Get detailed forecast for a location", + handler: (string location, int days, ILambdaContext ctx) => { + ctx.Logger.LogLine($"Getting forecast for {location}"); + return $"{days}-day forecast for {location}"; + } + ); + + resolver.Tool( + name: "Greet", + description: "Greet a user", + handler: (string name) => { + return $"Hello {name}"; + } + ); + + var input = new ActionGroupInvocationInput { - - return new ActionGroupInvocationOutput { Text = "2" }; - }); + Function = "GetCustomForecast", + Parameters = new List + { + new Parameter + { + Name = "location", + Value = "Lisbon", + Type = "String" + }, + new Parameter + { + Name = "days", + Value = "1", + Type = "Number" + } + } + }; var context = new TestLambdaContext(); // Act - var result = resolver.Resolve(_bedrockEvent, context); + var result = resolver.Resolve(input, context); // Assert - Assert.Equal("2", result.Text); + Assert.Equal("1-day forecast for Lisbon", result.Text); } } -// Types + // String // Number // Integer From 9b57437503a8846d7df56ff94211641071fa1523 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Sun, 25 May 2025 20:29:05 +0100 Subject: [PATCH 03/52] feat(BedrockAgentFunctionResolver): enhance tool registration with detailed XML documentation and support for various handler signatures --- .../BedrockAgentFunctionResolver.cs | 336 ++++++++++++++---- .../BedrockAgentFunctionResolverExtensions.cs | 28 +- .../AppSyncEventsTests.cs | 69 ++-- .../BedrockAgentFunctionResolverTests.cs | 331 +++++++++++++++-- .../RouteHandlerRegistryTests.cs | 8 +- 5 files changed, 639 insertions(+), 133 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs index 5f33caa9..c78911b3 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs @@ -1,14 +1,26 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text.Json.Serialization; -using System.Threading.Tasks; +using System.Globalization; using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; +// ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler { + /// + /// A resolver for Bedrock Agent functions that allows registering handlers for tool functions. + /// + /// + /// Basic usage: + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool("GetWeather", (string city) => $"Weather in {city} is sunny"); + /// + /// // Lambda handler + /// public ActionGroupInvocationOutput FunctionHandler(ActionGroupInvocationInput input, ILambdaContext context) + /// { + /// return resolver.Resolve(input, context); + /// } + /// + /// public class BedrockAgentFunctionResolver { private readonly @@ -33,6 +45,23 @@ private static bool IsBedrockParameter(Type type) => /// /// Registers a handler that directly accepts ActionGroupInvocationInput and returns ActionGroupInvocationOutput /// + /// The name of the tool function + /// The handler function that accepts input and context and returns output + /// Optional description of the tool function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool( + /// "GetWeatherDetails", + /// (ActionGroupInvocationInput input, ILambdaContext context) => { + /// context.Logger.LogLine($"Processing request for {input.Function}"); + /// return new ActionGroupInvocationOutput { Text = "Weather details response" }; + /// }, + /// "Gets detailed weather information" + /// ); + /// + /// public BedrockAgentFunctionResolver Tool( string name, Func handler, @@ -48,6 +77,23 @@ public BedrockAgentFunctionResolver Tool( /// /// Registers a handler that directly accepts ActionGroupInvocationInput and returns ActionGroupInvocationOutput /// + /// The name of the tool function + /// The handler function that accepts input and returns output + /// Optional description of the tool function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool( + /// "GetWeatherDetails", + /// (ActionGroupInvocationInput input) => { + /// var city = input.Parameters.FirstOrDefault(p => p.Name == "city")?.Value; + /// return new ActionGroupInvocationOutput { Text = $"Weather in {city} is sunny" }; + /// }, + /// "Gets weather for a city" + /// ); + /// + /// public BedrockAgentFunctionResolver Tool( string name, Func handler, @@ -61,24 +107,199 @@ public BedrockAgentFunctionResolver Tool( } /// - /// Registers a handler for a tool function with automatically converted return type. + /// Registers a parameter-less handler that returns ActionGroupInvocationOutput /// + /// The name of the tool function + /// The handler function that returns output + /// Optional description of the tool function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool( + /// "GetCurrentTime", + /// () => new ActionGroupInvocationOutput { Text = DateTime.Now.ToString() }, + /// "Gets the current server time" + /// ); + /// + /// public BedrockAgentFunctionResolver Tool( string name, - string description = "", - Delegate? handler = null) + Func handler, + string description = "") + { + ArgumentNullException.ThrowIfNull(handler); + + _handlers[name] = (input, context) => handler(); + return this; + } + + /// + /// Registers a parameter-less handler with automatic string conversion + /// + /// The name of the tool function + /// The handler function that returns a string + /// Optional description of the tool function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool( + /// "GetGreeting", + /// () => "Hello, world!", + /// "Returns a greeting message" + /// ); + /// + /// + public BedrockAgentFunctionResolver Tool( + string name, + Func handler, + string description = "") + { + ArgumentNullException.ThrowIfNull(handler); + + _handlers[name] = (input, context) => new ActionGroupInvocationOutput { Text = handler() }; + return this; + } + + /// + /// Registers a parameter-less handler with automatic object conversion + /// + /// The name of the tool function + /// The handler function that returns an object + /// Optional description of the tool function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool( + /// "GetServerStatus", + /// () => new { Status = "Online", Uptime = "99.9%" }, + /// "Returns the server status information" + /// ); + /// + /// + public BedrockAgentFunctionResolver Tool( + string name, + Func handler, + string description = "") + { + ArgumentNullException.ThrowIfNull(handler); + + _handlers[name] = (input, context) => + { + var result = handler(); + return ConvertToOutput(result); + }; + return this; + } + + /// + /// Registers a handler for a tool function with automatically converted return type (no description). + /// + /// The name of the tool function + /// The delegate handler function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool( + /// "CalculateSum", + /// (int a, int b) => a + b + /// ); + /// + /// + public BedrockAgentFunctionResolver Tool( + string name, + Delegate handler) + { + return Tool(name, "", handler); + } + + /// + /// Registers a handler for a tool function with description and automatically converted return type. + /// + /// The name of the tool function + /// Description of the tool function + /// The delegate handler function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool( + /// "GetWeather", + /// "Gets the weather forecast for a specific city", + /// (string city, int days) => $"{days}-day forecast for {city}: Sunny" + /// ); + /// + /// + public BedrockAgentFunctionResolver Tool( + string name, + string description, + Delegate handler) { - // Delegate to the generic version with object as return type return Tool(name, description, handler); } /// - /// Registers a handler for a tool function with typed return value. + /// Registers a handler for a tool function with typed return value (no description). + /// + /// The return type of the handler + /// The name of the tool function + /// The delegate handler function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool<int>( + /// "CalculateArea", + /// (int width, int height) => width * height + /// ); + /// + /// + public BedrockAgentFunctionResolver Tool( + string name, + Delegate handler) + { + return Tool(name, "", handler); + } + + /// + /// Registers a handler for a tool function with description and typed return value. /// + /// The return type of the handler + /// The name of the tool function + /// Description of the tool function + /// The delegate handler function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// + /// // Register a function with strongly typed parameters and return value + /// resolver.Tool<double>( + /// "CalculateDistance", + /// "Calculates the distance between two points", + /// (double x1, double y1, double x2, double y2) => { + /// return Math.Sqrt(Math.Pow(x2 - x1, 2) + Math.Pow(y2 - y1, 2)); + /// } + /// ); + /// + /// // Register a function that accepts Lambda context + /// resolver.Tool<string>( + /// "LogAndReturn", + /// "Logs a message and returns it", + /// (string message, ILambdaContext context) => { + /// context.Logger.LogLine($"Message received: {message}"); + /// return message; + /// } + /// ); + /// + /// public BedrockAgentFunctionResolver Tool( string name, - string description = "", - Delegate? handler = null) + string description, + Delegate handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); @@ -91,7 +312,7 @@ public BedrockAgentFunctionResolver Tool( var bedrockParamIndex = 0; // Get service provider from resolver if available - var serviceProvider = (this as DIBedrockAgentFunctionResolver)?.ServiceProvider; + var serviceProvider = (this as DiBedrockAgentFunctionResolver)?.ServiceProvider; // Map parameters from Bedrock input and DI for (var i = 0; i < parameters.Length; i++) @@ -197,58 +418,25 @@ public BedrockAgentFunctionResolver Tool( return this; } - /// - /// Registers a parameter-less handler that returns ActionGroupInvocationOutput - /// - public BedrockAgentFunctionResolver Tool( - string name, - Func handler, - string description = "") - { - if (handler == null) - throw new ArgumentNullException(nameof(handler)); - - _handlers[name] = (input, context) => handler(); - return this; - } - - /// - /// Registers a parameter-less handler with automatic string conversion - /// - public BedrockAgentFunctionResolver Tool( - string name, - Func handler, - string description = "") - { - if (handler == null) - throw new ArgumentNullException(nameof(handler)); - - _handlers[name] = (input, context) => new ActionGroupInvocationOutput { Text = handler() }; - return this; - } - - /// - /// Registers a parameter-less handler with automatic object conversion - /// - public BedrockAgentFunctionResolver Tool( - string name, - Func handler, - string description = "") - { - if (handler == null) - throw new ArgumentNullException(nameof(handler)); - - _handlers[name] = (input, context) => - { - var result = handler(); - return ConvertToOutput(result); - }; - return this; - } - /// /// Resolves and processes a Bedrock Agent function invocation. /// + /// The Bedrock Agent input containing the function name and parameters + /// Optional Lambda context + /// The output from the function execution + /// + /// + /// // Lambda handler + /// public ActionGroupInvocationOutput FunctionHandler(ActionGroupInvocationInput input, ILambdaContext context) + /// { + /// var resolver = new BedrockAgentFunctionResolver() + /// .Tool("GetWeather", (string city) => $"Weather in {city} is sunny") + /// .Tool("GetTime", () => DateTime.Now.ToString()); + /// + /// return resolver.Resolve(input, context); + /// } + /// + /// public ActionGroupInvocationOutput Resolve(ActionGroupInvocationInput input, ILambdaContext? context = null) { return ResolveAsync(input, context).GetAwaiter().GetResult(); @@ -257,6 +445,26 @@ public ActionGroupInvocationOutput Resolve(ActionGroupInvocationInput input, ILa /// /// Asynchronously resolves and processes a Bedrock Agent function invocation. /// + /// The Bedrock Agent input containing the function name and parameters + /// Optional Lambda context + /// A task that completes with the output from the function execution + /// + /// + /// // Async Lambda handler + /// public async Task<ActionGroupInvocationOutput> FunctionHandler(ActionGroupInvocationInput input, ILambdaContext context) + /// { + /// var resolver = new BedrockAgentFunctionResolver() + /// .Tool("GetWeatherAsync", async (string city) => { + /// // Simulate API call + /// await Task.Delay(100); + /// return $"Weather in {city} is sunny"; + /// }) + /// .Tool("GetTime", () => DateTime.Now.ToString()); + /// + /// return await resolver.ResolveAsync(input, context); + /// } + /// + /// public async Task ResolveAsync(ActionGroupInvocationInput input, ILambdaContext? context = null) { @@ -344,4 +552,4 @@ private ActionGroupInvocationOutput ConvertToOutput(T result) return new ActionGroupInvocationOutput { Text = result.ToString() ?? string.Empty }; } } -} \ No newline at end of file +} diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs index 003f86d2..871e01b3 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs @@ -1,26 +1,42 @@ -using System; using Microsoft.Extensions.DependencyInjection; +// ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler { - // Service provider-aware resolver - public class DIBedrockAgentFunctionResolver : BedrockAgentFunctionResolver + /// + /// Extended Bedrock Agent Function Resolver with dependency injection support. + /// + public class DiBedrockAgentFunctionResolver : BedrockAgentFunctionResolver { + /// + /// Gets the service provider used for dependency injection. + /// public IServiceProvider ServiceProvider { get; } - public DIBedrockAgentFunctionResolver(IServiceProvider serviceProvider) + /// + /// Initializes a new instance of the class. + /// + /// The service provider for dependency injection. + public DiBedrockAgentFunctionResolver(IServiceProvider serviceProvider) { ServiceProvider = serviceProvider; } } + /// + /// Extension methods for Bedrock Agent Function Resolver. + /// public static class BedrockResolverExtensions { - // Extension to register the resolver in DI + /// + /// Registers a Bedrock Agent Function Resolver with dependency injection support. + /// + /// The service collection to add the resolver to. + /// The updated service collection. public static IServiceCollection AddBedrockResolver(this IServiceCollection services) { services.AddSingleton(sp => - new DIBedrockAgentFunctionResolver(sp)); + new DiBedrockAgentFunctionResolver(sp)); return services; } } diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AppSyncEventsTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AppSyncEventsTests.cs index b4301f93..0b8103f4 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AppSyncEventsTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AppSyncEventsTests.cs @@ -3,6 +3,8 @@ using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.EventHandler.AppSyncEvents; +#pragma warning disable CS8604 // Possible null reference argument. +#pragma warning disable CS8602 // Dereference of a possibly null reference. namespace AWS.Lambda.Powertools.EventHandler.Tests; @@ -74,10 +76,10 @@ public async Task Should_Return_Unchanged_Payload_Async() var lambdaContext = new TestLambdaContext(); var app = new AppSyncEventsResolver(); - app.OnPublishAsync("/default/channel", async payload => + app.OnPublishAsync("/default/channel", payload => { // Handle channel1 events - return payload; + return Task.FromResult(payload); }); // Act @@ -101,7 +103,7 @@ public async Task Should_Handle_Error_In_Event_Processing() var lambdaContext = new TestLambdaContext(); var app = new AppSyncEventsResolver(); - app.OnPublishAsync("/default/channel", async (payload) => + app.OnPublishAsync("/default/channel", (payload) => { // Throw exception for second event if (payload.ContainsKey("event_2")) @@ -109,7 +111,7 @@ public async Task Should_Handle_Error_In_Event_Processing() throw new InvalidOperationException("Test error"); } - return payload; + return Task.FromResult(payload); }); // Act @@ -137,10 +139,10 @@ public async Task Should_Match_Path_With_Wildcard() var app = new AppSyncEventsResolver(); int callCount = 0; - app.OnPublishAsync("/default/*", async (payload) => + app.OnPublishAsync("/default/*", (payload) => { callCount++; - return new Dictionary { ["wildcard_matched"] = true }; + return Task.FromResult(new Dictionary { ["wildcard_matched"] = true }); }); // Act @@ -162,9 +164,9 @@ public async Task Should_Authorize_Subscription() var lambdaContext = new TestLambdaContext(); var app = new AppSyncEventsResolver(); - app.OnPublishAsync("/default/channel", async (payload) => payload); + app.OnPublishAsync("/default/channel", (payload) => Task.FromResult(payload)); - app.OnSubscribeAsync("/default/*", async (info) => true); + app.OnSubscribeAsync("/default/*", (info) => Task.FromResult(true)); var subscribeEvent = new AppSyncEventsRequest { Info = new Information @@ -263,8 +265,7 @@ public async Task Should_Handle_Error_In_Aggregate_Mode_Async() var lambdaContext = new TestLambdaContext(); var app = new AppSyncEventsResolver(); - app.OnPublishAggregateAsync("/default/channel", - async (evt, ctx) => { throw new InvalidOperationException("Aggregate error"); }); + app.OnPublishAggregateAsync("/default/channel", (evt, ctx) => { throw new InvalidOperationException("Aggregate error"); }); // Act var result = await app.ResolveAsync(_appSyncEvent, lambdaContext); @@ -311,7 +312,7 @@ public async Task Should_Handle_TransformingPayload_Async() var lambdaContext = new TestLambdaContext(); var app = new AppSyncEventsResolver(); - app.OnPublishAsync("/default/channel", async (payload) => + app.OnPublishAsync("/default/channel", (payload) => { // Transform each event payload var transformedPayload = new Dictionary(); @@ -320,7 +321,7 @@ public async Task Should_Handle_TransformingPayload_Async() transformedPayload[$"transformed_{key}"] = $"transformed_{payload[key]}"; } - return transformedPayload; + return Task.FromResult(transformedPayload); }); // Act @@ -462,11 +463,9 @@ public async Task Should_Replace_Handler_When_RegisteringTwice_Async() var lambdaContext = new TestLambdaContext(); var app = new AppSyncEventsResolver(); - app.OnPublishAsync("/default/channel", - async (payload) => { return new Dictionary { ["handler"] = "first" }; }); + app.OnPublishAsync("/default/channel", (payload) => { return Task.FromResult(new Dictionary { ["handler"] = "first" }); }); - app.OnPublishAsync("/default/channel", - async (payload) => { return new Dictionary { ["handler"] = "second" }; }); + app.OnPublishAsync("/default/channel", (payload) => { return Task.FromResult(new Dictionary { ["handler"] = "second" }); }); // Act var result = await app.ResolveAsync(_appSyncEvent, lambdaContext); @@ -513,7 +512,7 @@ public async Task Aggregate_Handler_Can_Return_Individual_Results_With_Ids() app.OnPublishAsync("/default/channel12", (payload) => { throw new Exception("My custom exception"); }); - app.OnPublishAggregateAsync("/default/channel", async (evt) => + app.OnPublishAggregateAsync("/default/channel", (evt) => { // Iterate through events and return individual results with IDs var results = new List(); @@ -555,7 +554,7 @@ public async Task Aggregate_Handler_Can_Return_Individual_Results_With_Ids() } } - return new AppSyncEventsResponse { Events = results }; + return Task.FromResult(new AppSyncEventsResponse { Events = results }); }); // Act @@ -583,13 +582,13 @@ public async Task Should_Verify_Ids_Are_Preserved_In_Error_Case() var app = new AppSyncEventsResolver(); // Create handlers that throw exceptions for specific events - app.OnPublishAsync("/default/channel", async (payload) => + app.OnPublishAsync("/default/channel", (payload) => { if (payload.ContainsKey("event_1")) throw new InvalidOperationException("Error for event 1"); if (payload.ContainsKey("event_3")) throw new ArgumentException("Error for event 3"); - return payload; + return Task.FromResult(payload); }); // Act @@ -615,16 +614,16 @@ public async Task Should_Match_Most_Specific_Handler_Only() int firstHandlerCalls = 0; int secondHandlerCalls = 0; - app.OnPublishAsync("/default/channel", async (payload) => + app.OnPublishAsync("/default/channel", (payload) => { firstHandlerCalls++; - return new Dictionary { ["handler"] = "first" }; + return Task.FromResult(new Dictionary { ["handler"] = "first" }); }); - app.OnPublishAsync("/default/*", async (payload) => + app.OnPublishAsync("/default/*", (payload) => { secondHandlerCalls++; - return new Dictionary { ["handler"] = "second" }; + return Task.FromResult(new Dictionary { ["handler"] = "second" }); }); // Act @@ -667,18 +666,18 @@ public async Task Should_Handle_Multiple_Keys_In_Payload() ] }; - app.OnPublishAsync("/default/channel", async (payload) => + app.OnPublishAsync("/default/channel", (payload) => { // Check that both keys are present Assert.Equal("data_1", payload["event_1"]); Assert.Equal("data_1a", payload["event_1a"]); // Return a processed result with both keys - return new Dictionary + return Task.FromResult(new Dictionary { ["processed_1"] = payload["event_1"], ["processed_1a"] = payload["event_1a"] - }; + }); }); // Act @@ -699,14 +698,11 @@ public async Task Should_Only_Use_First_Matching_Handler_By_Specificity() var app = new AppSyncEventsResolver(); // Register handlers with different specificity - app.OnPublishAsync("/*", async (payload) => - new Dictionary { ["handler"] = "least-specific" }); + app.OnPublishAsync("/*", (payload) => Task.FromResult(new Dictionary { ["handler"] = "least-specific" })); - app.OnPublishAsync("/default/*", async (payload) => - new Dictionary { ["handler"] = "more-specific" }); + app.OnPublishAsync("/default/*", (payload) => Task.FromResult(new Dictionary { ["handler"] = "more-specific" })); - app.OnPublishAsync("/default/channel", async (payload) => - new Dictionary { ["handler"] = "most-specific" }); + app.OnPublishAsync("/default/channel", (payload) => Task.FromResult(new Dictionary { ["handler"] = "most-specific" })); // Act var result = await app.ResolveAsync(_appSyncEvent, lambdaContext); @@ -744,8 +740,7 @@ public async Task Should_Fallback_To_Less_Specific_Handler_If_No_Exact_Match() ] }; - app.OnPublishAsync("/default/*", async (payload) => - new Dictionary { ["handler"] = "wildcard-handler" }); + app.OnPublishAsync("/default/*", (payload) => Task.FromResult(new Dictionary { ["handler"] = "wildcard-handler" })); // Act var result = await app.ResolveAsync(fallbackEvent, lambdaContext); @@ -763,7 +758,7 @@ public async Task Should_Return_Null_When_Subscribing_To_Path_Without_Publish_Ha var app = new AppSyncEventsResolver(); // Only set up a subscribe handler without corresponding publish handler - app.OnSubscribeAsync("/subscribe-only", async (info) => true); + app.OnSubscribeAsync("/subscribe-only", (info) => Task.FromResult(true)); var subscribeEvent = new AppSyncEventsRequest { @@ -824,7 +819,7 @@ public async Task Should_Return_UnauthorizedException_When_Throwing_Unauthorized var lambdaContext = new TestLambdaContext(); var app = new AppSyncEventsResolver(); - app.OnPublishAsync(publishPath, async (payload) => payload); + app.OnPublishAsync(publishPath, (payload) => Task.FromResult(payload)); app.OnSubscribeAsync(subscribePath, (info, lambdaContext) => { throw new UnauthorizedException("OOPS"); }); diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs index e30dcd9b..4e3ba51c 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs @@ -1,9 +1,11 @@ +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; using Microsoft.Extensions.DependencyInjection; +#pragma warning disable CS0162 // Unreachable code detected namespace AWS.Lambda.Powertools.EventHandler.Tests; @@ -102,7 +104,7 @@ public void TestFunctionHandlerWithInput() // Arrange var resolver = new BedrockAgentFunctionResolver(); resolver.Tool("TestFunction", - (ActionGroupInvocationInput input, ILambdaContext context) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); + (input, context) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); var input = new ActionGroupInvocationInput { Function = "TestFunction" }; var context = new TestLambdaContext(); @@ -120,7 +122,7 @@ public async Task TestFunctionHandlerWithInputAsync() // Arrange var resolver = new BedrockAgentFunctionResolver(); resolver.Tool("TestFunction", - (ActionGroupInvocationInput input) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); + input => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); var input = new ActionGroupInvocationInput { Function = "TestFunction" }; var context = new TestLambdaContext(); @@ -176,10 +178,6 @@ public void TestFunctionHandlerWithParameters() var input = new ActionGroupInvocationInput { Function = "TestFunction", - RequestBody = new RequestBody - { - - }, Parameters = new List { new Parameter @@ -267,11 +265,9 @@ public void TestFunctionHandlerWithEvent() [Fact] public void TestFunctionHandlerWithEventAndServices() { - // Arrange - // Setup DI var services = new ServiceCollection(); - services.AddSingleton(new HttpClient()); + services.AddSingleton(new MyImplementation()); services.AddBedrockResolver(); var serviceProvider = services.BuildServiceProvider(); @@ -280,21 +276,13 @@ public void TestFunctionHandlerWithEventAndServices() resolver.Tool( name: "GetCustomForecast", description: "Get detailed forecast for a location", - handler: async (string location, int days, HttpClient client, ILambdaContext ctx) => + handler: async (string location, int days, IMyInterface client, ILambdaContext ctx) => { - var resp = await client.GetStringAsync("https://api.open-meteo.com/v1/forecast?latitude=38.7167&longitude=-9.1333¤t=temperature_2m"); + var resp = await client.DoSomething(location , days); return resp; } ); - resolver.Tool( - name: "Greet", - description: "Greet a user", - handler: (string name) => { - return $"Hello {name}"; - } - ); - var input = new ActionGroupInvocationInput { Function = "GetCustomForecast", @@ -321,7 +309,7 @@ public void TestFunctionHandlerWithEventAndServices() var result = resolver.Resolve(input, context); // Assert - Assert.Equal("1-day forecast for Lisbon", result.Text); + Assert.Equal("Forecast for Lisbon for 1 days", result.Text); } [Fact] @@ -374,11 +362,304 @@ public void TestFunctionHandlerWithEventTypes() // Assert Assert.Equal("1-day forecast for Lisbon", result.Text); } + + [Fact] + public void TestFunctionHandlerWithBooleanParameter() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "TestBool", + description: "Test boolean parameter", + handler: (bool isEnabled) => { + return $"Feature is {(isEnabled ? "enabled" : "disabled")}"; + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "TestBool", + Parameters = new List + { + new Parameter + { + Name = "isEnabled", + Value = "true", + Type = "Boolean" + } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Feature is enabled", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithMissingRequiredParameter() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "RequiredParam", + description: "Function with required parameter", + handler: (string name) => $"Hello, {name}!" + ); + + var input = new ActionGroupInvocationInput + { + Function = "RequiredParam", + Parameters = new List() // Empty parameters + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains("Hello, !", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithMultipleParameterTypes() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "ComplexFunction", + description: "Test multiple parameter types", + handler: (string name, int count, bool isActive) => { + return $"Name: {name}, Count: {count}, Active: {isActive}"; + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "ComplexFunction", + Parameters = new List + { + new Parameter { Name = "name", Value = "Test", Type = "String" }, + new Parameter { Name = "count", Value = "5", Type = "Integer" }, + new Parameter { Name = "isActive", Value = "true", Type = "Boolean" } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Name: Test, Count: 5, Active: True", result.Text); + } + + public enum TestEnum + { + Option1, + Option2, + Option3 + } + + [Fact] + public void TestFunctionHandlerWithEnumParameter() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "EnumTest", + description: "Test enum parameter", + handler: (TestEnum option) => { + return $"Selected option: {option}"; + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "EnumTest", + Parameters = new List + { + new Parameter + { + Name = "option", + Value = "Option2", + Type = "String" // Enums come as strings + } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Selected option: Option2", result.Text); + } + + [Fact] + public void TestParameterNameCaseSensitivity() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "CaseTest", + description: "Test case sensitivity", + handler: (string userName) => $"Hello, {userName}!" + ); + + var input = new ActionGroupInvocationInput + { + Function = "CaseTest", + Parameters = new List + { + new Parameter + { + Name = "UserName", // Different case than parameter + Value = "John", + Type = "String" + } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Hello, John!", result.Text); + } + + [Fact] + public void TestParameterOrderIndependence() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "OrderTest", + description: "Test parameter order independence", + handler: (string firstName, string lastName) => { + return $"Name: {firstName} {lastName}"; + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "OrderTest", + Parameters = new List + { + // Parameters in reverse order of handler parameters + new Parameter { Name = "lastName", Value = "Smith", Type = "String" }, + new Parameter { Name = "firstName", Value = "John", Type = "String" } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Name: John Smith", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithDecimalParameter() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "PriceCalculator", + description: "Calculate total price with tax", + handler: (decimal price) => { + var withTax = price * 1.2m; + return $"Total price with tax: {withTax.ToString("F2", CultureInfo.InvariantCulture)}"; + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "PriceCalculator", + Parameters = new List + { + new Parameter + { + Name = "price", + Value = "29.99", + Type = "Number" + } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains("35.99", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithArrayParameter() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "ArrayTest", + description: "Test with array parameter", + handler: (string text) => { + // In a real implementation, you'd parse the array from the string + // ActionGroupInvocationInput doesn't directly support array types + return $"Received: {text}"; + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "ArrayTest", + Parameters = new List + { + new Parameter + { + Name = "text", + Value = "[\"item1\",\"item2\"]", // Array as JSON string + Type = "Array" + } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Received: [\"item1\",\"item2\"]", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithExceptionInHandler() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "ThrowingFunction", + description: "Function that throws exception", + handler: () => { + throw new InvalidOperationException("Test error"); + return "This will not run:"; + } + ); + + var input = new ActionGroupInvocationInput { Function = "ThrowingFunction" }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains("Error executing function", result.Text); + } } +internal interface IMyInterface +{ + Task DoSomething(string location, int days); +} -// String -// Number -// Integer -// Boolean -// Array \ No newline at end of file +internal class MyImplementation : IMyInterface +{ + public async Task DoSomething(string location, int days) + { + return await Task.FromResult($"Forecast for {location} for {days} days"); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/RouteHandlerRegistryTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/RouteHandlerRegistryTests.cs index 92c9da3a..f0437dc9 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/RouteHandlerRegistryTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/RouteHandlerRegistryTests.cs @@ -1,7 +1,13 @@ +using System.Diagnostics.CodeAnalysis; using AWS.Lambda.Powertools.EventHandler.Internal; +#pragma warning disable CS8605 // Unboxing a possibly null value. +#pragma warning disable CS8601 // Possible null reference assignment. +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. +#pragma warning disable CS8602 // Dereference of a possibly null reference. namespace AWS.Lambda.Powertools.EventHandler.Tests; +[SuppressMessage("Usage", "xUnit1031:Do not use blocking task operations in test method")] public class RouteHandlerRegistryTests { [Theory] @@ -14,7 +20,7 @@ public class RouteHandlerRegistryTests [InlineData("default/*", false)] // Not starting with slash [InlineData("", false)] // Empty path [InlineData(null, false)] // Null path - public void IsValidPath_ShouldValidateCorrectly(string path, bool expected) + public void IsValidPath_ShouldValidateCorrectly(string? path, bool expected) { // Create a private method accessor to test private IsValidPath method var registry = new RouteHandlerRegistry(); From d84910f745745e10c80903ecc704cf5db28a6531 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 26 May 2025 12:40:13 +0100 Subject: [PATCH 04/52] add array parsing and documentation --- .../event_handler/bedrock_agent_function.md | 307 ++++++++++++++++++ .../BedrockAgentFunctionResolver.cs | 58 +++- .../Readme.md | 307 ++++++++++++++++-- .../BedrockAgentFunctionResolverTests.cs | 204 +++++++++--- mkdocs.yml | 1 + 5 files changed, 793 insertions(+), 84 deletions(-) create mode 100644 docs/core/event_handler/bedrock_agent_function.md diff --git a/docs/core/event_handler/bedrock_agent_function.md b/docs/core/event_handler/bedrock_agent_function.md new file mode 100644 index 00000000..0b2b866e --- /dev/null +++ b/docs/core/event_handler/bedrock_agent_function.md @@ -0,0 +1,307 @@ +--- +title: Bedrock Agent Function Resolver +description: Event Handler - Bedrock Agent Function Resolver +--- + +# 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. + +## Features + +- **Simple Tool Registration**: Register functions with descriptive names that Bedrock Agents can invoke +- **Automatic Parameter Handling**: Parameters are automatically extracted from Bedrock Agent requests and converted to the appropriate types +- **Type Safety**: Strongly typed parameters and return values +- **Multiple Return Types**: Support for returning strings, primitive types, objects, or custom types +- **Flexible Input Options**: Support for various parameter types including string, int, bool, DateTime, and enums +- **Lambda Context Access**: Easy access to Lambda context for logging and AWS Lambda features +- **Dependency Injection Support**: Seamless integration with .NET's dependency injection system + +## Installation + +Install the package via NuGet: + +```bash +dotnet add package AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver +``` + +## Basic Usage + +Here's a simple example showing how to register and use tool functions: + +```csharp +using Amazon.BedrockAgentRuntime.Model; +using Amazon.Lambda.Core; +using AWS.Lambda.Powertools.EventHandler; + +[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 ActionGroupInvocationOutput FunctionHandler( + ActionGroupInvocationInput 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. + +## Advanced Usage + +### Functions with Descriptions + +Add descriptive information to your tool functions: + +```csharp +_resolver.Tool( + "CheckInventory", + "Checks if a product is available in inventory", + (string productId, bool checkWarehouse) => + { + return checkWarehouse + ? $"Product {productId} has 15 units in warehouse" + : $"Product {productId} has 5 units in store"; + }); +``` + +### Accessing Lambda Context + +Access the Lambda context in your functions: + +```csharp +_resolver.Tool( + "LogRequest", + "Logs request information and returns confirmation", + (string requestId, ILambdaContext context) => + { + context.Logger.LogLine($"Processing request {requestId}"); + return $"Request {requestId} logged successfully"; + }); +``` + +### Working with Complex Return Types + +Return complex objects that will be converted to appropriate responses: + +```csharp +public class WeatherReport +{ + public string City { get; set; } + public string Conditions { get; set; } + public int Temperature { get; set; } + + public override string ToString() + { + return $"Weather in {City}: {Conditions}, {Temperature}°F"; + } +} + +_resolver.Tool( + "GetDetailedWeather", + "Returns detailed weather information for a location", + (string city) => new WeatherReport + { + City = city, + Conditions = "Partly Cloudy", + Temperature = 72 + }); +``` + +### Asynchronous Functions + +Register and use asynchronous functions: + +```csharp +_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: + +```csharp +_resolver.Tool( + "ProcessRawRequest", + "Processes the raw Bedrock Agent request", + (ActionGroupInvocationInput 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: + +```csharp +using Microsoft.Extensions.DependencyInjection; + +// Set up dependency injection +var services = new ServiceCollection(); +services.AddSingleton(); +services.AddBedrockResolver(); // Extension method to register the resolver + +var serviceProvider = services.BuildServiceProvider(); +var resolver = serviceProvider.GetRequiredService(); + +// 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); + }); +``` + +## 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: + - Finds the registered handler for the requested function + - Extracts and converts parameters to the correct types + - Invokes your handler with the parameters + - Formats the response in the way Bedrock Agents expect +5. The agent receives the response and uses it to continue the conversation with the user + +## Supported Parameter Types + +- `string` +- `int` / `long` +- `double` / `decimal` +- `bool` +- `DateTime` +- `Guid` +- `enum` types +- `ILambdaContext` (for accessing Lambda context) +- `ActionGroupInvocationInput` (for accessing raw request) +- Any service registered in dependency injection + +## Benefits + +- **Reduced Boilerplate**: Eliminate repetitive code for parsing requests and formatting responses +- **Type Safety**: Strong typing for parameters and return values +- **Simplified Development**: Focus on business logic instead of request/response handling +- **Reusable Components**: Build a library of tool functions that can be shared across agents +- **Easy Testing**: Functions can be easily unit tested in isolation +- **Flexible Integration**: Works seamlessly with AWS Lambda and Bedrock Agents + +## Complete Example with Dependency Injection + +```csharp +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(); + services.AddSingleton(); + services.AddBedrockResolver(); // Extension method to register the resolver + + var serviceProvider = services.BuildServiceProvider(); + _resolver = serviceProvider.GetRequiredService(); + + // 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); + } + } +} +``` + +## Learn More + +For more information about Amazon Bedrock Agents and function integration, see the [Amazon Bedrock documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-tools.html). diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs index c78911b3..bcf61e67 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs @@ -1,10 +1,22 @@ using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; // ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler { + [JsonSerializable(typeof(string[]))] + [JsonSerializable(typeof(int[]))] + [JsonSerializable(typeof(long[]))] + [JsonSerializable(typeof(double[]))] + [JsonSerializable(typeof(bool[]))] + [JsonSerializable(typeof(decimal[]))] + internal partial class BedrockFunctionResolverContext : JsonSerializerContext + { + } + /// /// A resolver for Bedrock Agent functions that allows registering handlers for tool functions. /// @@ -36,11 +48,18 @@ private readonly typeof(bool), typeof(decimal), typeof(DateTime), - typeof(Guid) + typeof(Guid), + typeof(string[]), + typeof(int[]), + typeof(long[]), + typeof(double[]), + typeof(bool[]), + typeof(decimal[]) }; private static bool IsBedrockParameter(Type type) => - _bedrockParameterTypes.Contains(type) || type.IsEnum; + _bedrockParameterTypes.Contains(type) || type.IsEnum || + (type.IsArray && _bedrockParameterTypes.Contains(type.GetElementType()!)); /// /// Registers a handler that directly accepts ActionGroupInvocationInput and returns ActionGroupInvocationOutput @@ -333,6 +352,41 @@ public BedrockAgentFunctionResolver Tool( var paramName = parameter.Name ?? $"arg{bedrockParamIndex}"; // AOT-compatible parameter access - direct type checks + // Array parameter handling + if (paramType.IsArray) + { + var jsonArrayStr = accessor.Get(paramName); + + if (!string.IsNullOrEmpty(jsonArrayStr)) + { + try + { + // AOT-compatible deserialization using source generation + if (paramType == typeof(string[])) + args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.StringArray); + else if (paramType == typeof(int[])) + args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.Int32Array); + else if (paramType == typeof(long[])) + args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.Int64Array); + else if (paramType == typeof(double[])) + args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.DoubleArray); + else if (paramType == typeof(bool[])) + args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.BooleanArray); + else if (paramType == typeof(decimal[])) + args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.DecimalArray); + else + args[i] = null; // Unsupported array type + } + catch (JsonException) + { + args[i] = null; + } + } + else + { + args[i] = null; + } + } if (paramType == typeof(string)) args[i] = accessor.Get(paramName); else if (paramType == typeof(int)) diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md index 08947c02..decd8abb 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md @@ -1,34 +1,38 @@ # AWS Lambda Powertools for .NET - Bedrock Agent Function Resolver ## Overview -The Bedrock Agent Function Resolver is a custom function resolver for AWS Lambda Powertools for .NET. It is designed to work with the Bedrock Agent, a tool that simplifies the process of building and deploying serverless applications on AWS Lambda. -The Bedrock Agent Function Resolver allows you to easily resolve and invoke Lambda functions using the Bedrock Agent's conventions and best practices. -This custom function resolver is part of the AWS Lambda Powertools for .NET library, which provides a suite of utilities for building serverless applications on AWS Lambda. +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. + ## Features -- Custom function resolver for AWS Lambda Powertools for .NET -- Supports Bedrock Agent conventions and best practices -- Simplifies the process of resolving and invoking Lambda functions -- Integrates with AWS Lambda Powertools for .NET library -- Supports dependency injection and configuration -- Provides a consistent and easy-to-use API for resolving functions -- Supports asynchronous and synchronous function invocation -- Supports error handling and logging -- Supports custom serialization and deserialization -- Supports custom middleware and filters - -## Getting Started -To get started with the Bedrock Agent Function Resolver, you need to install the AWS Lambda Powertools for .NET library and the Bedrock Agent Function Resolver package. You can do this using NuGet: + +- **Simple Tool Registration**: Register functions with descriptive names that Bedrock Agents can invoke +- **Automatic Parameter Handling**: Parameters are automatically extracted from Bedrock Agent requests and converted to the appropriate types +- **Type Safety**: Strongly typed parameters and return values +- **Multiple Return Types**: Support for returning strings, primitive types, objects, or custom types +- **Flexible Input Options**: Support for various parameter types including string, int, bool, DateTime, and enums +- **Lambda Context Access**: Easy access to Lambda context for logging and AWS Lambda features +- **Dependency Injection Support**: Seamless integration with .NET's dependency injection system +- **Error Handling**: Automatic error capturing and formatting for responses +- **Async Support**: First-class support for asynchronous function execution + +## Installation + +Install the package via NuGet: ```bash dotnet add package AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver ``` -## Usage -To use the Bedrock Agent Function Resolver, you need to create an instance of the `BedrockAgentFunctionResolver` class and register it with the AWS Lambda Powertools for .NET library. You can do this in your Lambda function's entry point: + +## Basic Usage + +Here's a simple example showing how to register and use tool functions: ```csharp +using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; -using Amazon.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver; - +using AWS.Lambda.Powertools.EventHandler; [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] @@ -36,22 +40,265 @@ namespace MyLambdaFunction { public class Function { - private readonly BedrockAgentFunctionResolver _functionResolver; - + private readonly BedrockAgentFunctionResolver _resolver; + public Function() { - // Create an instance of the Bedrock Agent Function Resolver - _functionResolver = new BedrockAgentFunctionResolver(); + _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 ActionGroupInvocationOutput FunctionHandler( + ActionGroupInvocationInput 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. + +## Advanced Usage + +### Functions with Descriptions + +Add descriptive information to your tool functions: + +```csharp +_resolver.Tool( + "CheckInventory", + "Checks if a product is available in inventory", + (string productId, bool checkWarehouse) => + { + return checkWarehouse + ? $"Product {productId} has 15 units in warehouse" + : $"Product {productId} has 5 units in store"; + }); +``` + +### Accessing Lambda Context - public async Task FunctionHandler(ILambdaContext context) +Access the Lambda context in your functions: + +```csharp +_resolver.Tool( + "LogRequest", + "Logs request information and returns confirmation", + (string requestId, ILambdaContext context) => + { + context.Logger.LogLine($"Processing request {requestId}"); + return $"Request {requestId} logged successfully"; + }); +``` + +### Working with Complex Return Types + +Return complex objects that will be converted to appropriate responses: + +```csharp +public class WeatherReport +{ + public string City { get; set; } + public string Conditions { get; set; } + public int Temperature { get; set; } + + public override string ToString() + { + return $"Weather in {City}: {Conditions}, {Temperature}°F"; + } +} + +_resolver.Tool( + "GetDetailedWeather", + "Returns detailed weather information for a location", + (string city) => new WeatherReport + { + City = city, + Conditions = "Partly Cloudy", + Temperature = 72 + }); +``` + +### Asynchronous Functions + +Register and use asynchronous functions: + +```csharp +_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: + +```csharp +_resolver.Tool( + "ProcessRawRequest", + "Processes the raw Bedrock Agent request", + (ActionGroupInvocationInput 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: + +```csharp +using Microsoft.Extensions.DependencyInjection; + +// Set up dependency injection +var services = new ServiceCollection(); +services.AddSingleton(); +services.AddBedrockResolver(); // Extension method to register the resolver + +var serviceProvider = services.BuildServiceProvider(); +var resolver = serviceProvider.GetRequiredService(); + +// 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); + }); +``` + +## 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: + - Finds the registered handler for the requested function + - Extracts and converts parameters to the correct types + - Invokes your handler with the parameters + - Formats the response in the way Bedrock Agents expect +5. The agent receives the response and uses it to continue the conversation with the user + +## Supported Parameter Types + +- `string` +- `int` / `long` +- `double` / `decimal` +- `bool` +- `DateTime` +- `Guid` +- `enum` types +- `ILambdaContext` (for accessing Lambda context) +- `ActionGroupInvocationInput` (for accessing raw request) +- Any service registered in dependency injection + +## Benefits + +- **Reduced Boilerplate**: Eliminate repetitive code for parsing requests and formatting responses +- **Type Safety**: Strong typing for parameters and return values +- **Simplified Development**: Focus on business logic instead of request/response handling +- **Reusable Components**: Build a library of tool functions that can be shared across agents +- **Easy Testing**: Functions can be easily unit tested in isolation +- **Flexible Integration**: Works seamlessly with AWS Lambda and Bedrock Agents + +## Complete Example with Dependency Injection + +```csharp +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() { - // Use the function resolver to resolve and invoke a Lambda function - var result = await _functionResolver.ResolveAndInvokeAsync("MyLambdaFunctionName", new { /* input parameters */ }); + // Set up dependency injection + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddSingleton(); + services.AddBedrockResolver(); // Extension method to register the resolver - // Process the result - context.Logger.LogLine($"Result: {result}"); + var serviceProvider = services.BuildServiceProvider(); + _resolver = serviceProvider.GetRequiredService(); + + // 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); } } } -``` \ No newline at end of file +``` + +## Learn More + +For more information about Amazon Bedrock Agents and function integration, see the [Amazon Bedrock documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-tools.html). diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs index 4e3ba51c..0b0da0af 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs @@ -1,10 +1,12 @@ using System.Globalization; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; using Microsoft.Extensions.DependencyInjection; + #pragma warning disable CS0162 // Unreachable code detected namespace AWS.Lambda.Powertools.EventHandler.Tests; @@ -23,7 +25,7 @@ public BedrockAgentFunctionResolverTests() Converters = { new JsonStringEnumConverter() } })!; } - + [Fact] public void TestFunctionHandlerWithNoParameters() { @@ -202,7 +204,7 @@ public void TestFunctionHandlerWithParameters() // Assert Assert.Equal("Hello, World!", result.Text); } - + [Fact] public void TestFunctionHandlerWithEvent() { @@ -211,28 +213,25 @@ public void TestFunctionHandlerWithEvent() resolver.Tool( name: "GetCustomForecast", description: "Get detailed forecast for a location", - handler: (string location, int days, ILambdaContext ctx) => { + handler: (string location, int days, ILambdaContext ctx) => + { ctx.Logger.LogLine($"Getting forecast for {location}"); return $"{days}-day forecast for {location}"; } ); - + resolver.Tool( name: "Greet", description: "Greet a user", - handler: (string name) => { - return $"Hello {name}"; - } + handler: (string name) => { return $"Hello {name}"; } ); - + resolver.Tool( name: "Simple", description: "Greet a user", - handler: () => { - return "Hello"; - } + handler: () => { return "Hello"; } ); - + var input = new ActionGroupInvocationInput { Function = "GetCustomForecast", @@ -252,7 +251,7 @@ public void TestFunctionHandlerWithEvent() } } }; - + var context = new TestLambdaContext(); // Act @@ -261,7 +260,7 @@ public void TestFunctionHandlerWithEvent() // Assert Assert.Equal("1-day forecast for Lisbon", result.Text); } - + [Fact] public void TestFunctionHandlerWithEventAndServices() { @@ -272,17 +271,17 @@ public void TestFunctionHandlerWithEventAndServices() var serviceProvider = services.BuildServiceProvider(); var resolver = serviceProvider.GetRequiredService(); - + resolver.Tool( name: "GetCustomForecast", description: "Get detailed forecast for a location", handler: async (string location, int days, IMyInterface client, ILambdaContext ctx) => { - var resp = await client.DoSomething(location , days); + var resp = await client.DoSomething(location, days); return resp; } ); - + var input = new ActionGroupInvocationInput { Function = "GetCustomForecast", @@ -302,7 +301,7 @@ public void TestFunctionHandlerWithEventAndServices() } } }; - + var context = new TestLambdaContext(); // Act @@ -311,7 +310,7 @@ public void TestFunctionHandlerWithEventAndServices() // Assert Assert.Equal("Forecast for Lisbon for 1 days", result.Text); } - + [Fact] public void TestFunctionHandlerWithEventTypes() { @@ -320,20 +319,19 @@ public void TestFunctionHandlerWithEventTypes() resolver.Tool( name: "GetCustomForecast", description: "Get detailed forecast for a location", - handler: (string location, int days, ILambdaContext ctx) => { + handler: (string location, int days, ILambdaContext ctx) => + { ctx.Logger.LogLine($"Getting forecast for {location}"); return $"{days}-day forecast for {location}"; } ); - + resolver.Tool( name: "Greet", description: "Greet a user", - handler: (string name) => { - return $"Hello {name}"; - } + handler: (string name) => { return $"Hello {name}"; } ); - + var input = new ActionGroupInvocationInput { Function = "GetCustomForecast", @@ -353,7 +351,7 @@ public void TestFunctionHandlerWithEventTypes() } } }; - + var context = new TestLambdaContext(); // Act @@ -362,7 +360,7 @@ public void TestFunctionHandlerWithEventTypes() // Assert Assert.Equal("1-day forecast for Lisbon", result.Text); } - + [Fact] public void TestFunctionHandlerWithBooleanParameter() { @@ -371,9 +369,7 @@ public void TestFunctionHandlerWithBooleanParameter() resolver.Tool( name: "TestBool", description: "Test boolean parameter", - handler: (bool isEnabled) => { - return $"Feature is {(isEnabled ? "enabled" : "disabled")}"; - } + handler: (bool isEnabled) => { return $"Feature is {(isEnabled ? "enabled" : "disabled")}"; } ); var input = new ActionGroupInvocationInput @@ -396,7 +392,7 @@ public void TestFunctionHandlerWithBooleanParameter() // Assert Assert.Equal("Feature is enabled", result.Text); } - + [Fact] public void TestFunctionHandlerWithMissingRequiredParameter() { @@ -420,7 +416,7 @@ public void TestFunctionHandlerWithMissingRequiredParameter() // Assert Assert.Contains("Hello, !", result.Text); } - + [Fact] public void TestFunctionHandlerWithMultipleParameterTypes() { @@ -429,7 +425,8 @@ public void TestFunctionHandlerWithMultipleParameterTypes() resolver.Tool( name: "ComplexFunction", description: "Test multiple parameter types", - handler: (string name, int count, bool isActive) => { + handler: (string name, int count, bool isActive) => + { return $"Name: {name}, Count: {count}, Active: {isActive}"; } ); @@ -451,7 +448,7 @@ public void TestFunctionHandlerWithMultipleParameterTypes() // Assert Assert.Equal("Name: Test, Count: 5, Active: True", result.Text); } - + public enum TestEnum { Option1, @@ -467,9 +464,7 @@ public void TestFunctionHandlerWithEnumParameter() resolver.Tool( name: "EnumTest", description: "Test enum parameter", - handler: (TestEnum option) => { - return $"Selected option: {option}"; - } + handler: (TestEnum option) => { return $"Selected option: {option}"; } ); var input = new ActionGroupInvocationInput @@ -492,7 +487,7 @@ public void TestFunctionHandlerWithEnumParameter() // Assert Assert.Equal("Selected option: Option2", result.Text); } - + [Fact] public void TestParameterNameCaseSensitivity() { @@ -524,7 +519,7 @@ public void TestParameterNameCaseSensitivity() // Assert Assert.Equal("Hello, John!", result.Text); } - + [Fact] public void TestParameterOrderIndependence() { @@ -533,9 +528,7 @@ public void TestParameterOrderIndependence() resolver.Tool( name: "OrderTest", description: "Test parameter order independence", - handler: (string firstName, string lastName) => { - return $"Name: {firstName} {lastName}"; - } + handler: (string firstName, string lastName) => { return $"Name: {firstName} {lastName}"; } ); var input = new ActionGroupInvocationInput @@ -555,7 +548,7 @@ public void TestParameterOrderIndependence() // Assert Assert.Equal("Name: John Smith", result.Text); } - + [Fact] public void TestFunctionHandlerWithDecimalParameter() { @@ -564,7 +557,8 @@ public void TestFunctionHandlerWithDecimalParameter() resolver.Tool( name: "PriceCalculator", description: "Calculate total price with tax", - handler: (decimal price) => { + handler: (decimal price) => + { var withTax = price * 1.2m; return $"Total price with tax: {withTax.ToString("F2", CultureInfo.InvariantCulture)}"; } @@ -590,7 +584,7 @@ public void TestFunctionHandlerWithDecimalParameter() // Assert Assert.Contains("35.99", result.Text); } - + [Fact] public void TestFunctionHandlerWithArrayParameter() { @@ -599,7 +593,8 @@ public void TestFunctionHandlerWithArrayParameter() resolver.Tool( name: "ArrayTest", description: "Test with array parameter", - handler: (string text) => { + handler: (string text) => + { // In a real implementation, you'd parse the array from the string // ActionGroupInvocationInput doesn't directly support array types return $"Received: {text}"; @@ -615,7 +610,7 @@ public void TestFunctionHandlerWithArrayParameter() { Name = "text", Value = "[\"item1\",\"item2\"]", // Array as JSON string - Type = "Array" + Type = "Array" } } }; @@ -626,7 +621,111 @@ public void TestFunctionHandlerWithArrayParameter() // Assert Assert.Equal("Received: [\"item1\",\"item2\"]", result.Text); } - + + [Fact] + public void TestFunctionHandlerWithStringArrayParameter() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "ProcessWorkout", + description: "Process workout exercises", + handler: (string[] exercises) => + { + var result = new StringBuilder(); + result.AppendLine("Your workout plan:"); + + for (int i = 0; i < exercises.Length; i++) + { + result.AppendLine($" {i + 1}. {exercises[i]}"); + } + + return result.ToString(); + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "ProcessWorkout", + Parameters = new List + { + new Parameter + { + Name = "exercises", + Value = + "[\"Squats, 3 sets of 10 reps\",\"Push-ups, 3 sets of 10 reps\",\"Plank, 3 sets of 30 seconds\"]", + Type = "String" // The type is String since it contains JSON + } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains("Your workout plan:", result.Text); + Assert.Contains("1. Squats, 3 sets of 10 reps", result.Text); + Assert.Contains("2. Push-ups, 3 sets of 10 reps", result.Text); + Assert.Contains("3. Plank, 3 sets of 30 seconds", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithStringArrayParameterManualParse() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "ProcessWorkout", + description: "Process workout exercises", + handler: (ActionGroupInvocationInput input) => + { + // Manual array parsing since the resolver doesn't natively support arrays + var exercisesJson = input.Parameters.FirstOrDefault(p => p.Name == "exercises")?.Value ?? "[]"; + + // Parse JSON array + var exercises = JsonSerializer.Deserialize(exercisesJson); + + // Process the array items + var result = new StringBuilder(); + result.AppendLine("Your workout plan:"); + + if (exercises != null) + { + for (int i = 0; i < exercises.Length; i++) + { + result.AppendLine($" {i + 1}. {exercises[i]}"); + } + } + + return result.ToString(); + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "ProcessWorkout", + Parameters = new List + { + new Parameter + { + Name = "exercises", + Value = + "[\"Squats, 3 sets of 10 reps\",\"Push-ups, 3 sets of 10 reps\",\"Plank, 3 sets of 30 seconds\"]", + Type = "String" // The type is still String even though it contains JSON + } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains("Your workout plan:", result.Text); + Assert.Contains("1. Squats, 3 sets of 10 reps", result.Text); + Assert.Contains("2. Push-ups, 3 sets of 10 reps", result.Text); + Assert.Contains("3. Plank, 3 sets of 30 seconds", result.Text); + } + [Fact] public void TestFunctionHandlerWithExceptionInHandler() { @@ -635,17 +734,18 @@ public void TestFunctionHandlerWithExceptionInHandler() resolver.Tool( name: "ThrowingFunction", description: "Function that throws exception", - handler: () => { + handler: () => + { throw new InvalidOperationException("Test error"); return "This will not run:"; } ); - + var input = new ActionGroupInvocationInput { Function = "ThrowingFunction" }; - + // Act var result = resolver.Resolve(input); - + // Assert Assert.Contains("Error executing function", result.Text); } diff --git a/mkdocs.yml b/mkdocs.yml index a86866a4..97f2fccd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,6 +26,7 @@ nav: - utilities/batch-processing.md - Event Handler: - core/event_handler/appsync_events.md + - core/event_handler/bedrock_agent_function.md - utilities/parameters.md - utilities/jmespath-functions.md - API Reference: api/" target="_blank From 52d416a2accaf06d87927205650ef9d84c0f228a Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 26 May 2025 12:42:48 +0100 Subject: [PATCH 05/52] add new utility to version --- version.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/version.json b/version.json index 0b56b18d..759ed8a3 100644 --- a/version.json +++ b/version.json @@ -9,6 +9,7 @@ "Parameters": "1.3.1", "Idempotency": "1.3.0", "BatchProcessing": "1.2.1", - "EventHandler": "1.0.0" + "EventHandler": "1.0.0", + "BedrockAgentFunctionResolver": "1.0.0" } } From f7ec7a8a011add89d093d25fe2b2a0359cb38b95 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 27 May 2025 12:18:47 +0100 Subject: [PATCH 06/52] namespace refactor --- libraries/AWS.Lambda.Powertools.sln | 2 +- ...ler.Resolvers.BedrockAgentFunction.csproj} | 4 +- .../BedrockAgentFunctionResolver.cs | 2 +- .../BedrockAgentFunctionResolverExtensions.cs | 4 +- .../InternalsVisibleTo.cs | 0 .../ParameterAccessor.cs | 3 +- .../Readme.md | 0 ...ambda.Powertools.EventHandler.Tests.csproj | 6 ++- .../BedrockAgentFunctionResolverTests.cs | 41 ++++++++++++------- .../bedrockFunctionEvent2.json | 27 ++++++++++++ 10 files changed, 66 insertions(+), 23 deletions(-) rename libraries/src/{AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj => AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj} (71%) rename libraries/src/{AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver => AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction}/BedrockAgentFunctionResolver.cs (99%) rename libraries/src/{AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver => AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction}/BedrockAgentFunctionResolverExtensions.cs (91%) rename libraries/src/{AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver => AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction}/InternalsVisibleTo.cs (100%) rename libraries/src/{AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver => AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction}/ParameterAccessor.cs (97%) rename libraries/src/{AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver => AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction}/Readme.md (100%) create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json diff --git a/libraries/AWS.Lambda.Powertools.sln b/libraries/AWS.Lambda.Powertools.sln index 0ca62f24..056b3801 100644 --- a/libraries/AWS.Lambda.Powertools.sln +++ b/libraries/AWS.Lambda.Powertools.sln @@ -109,7 +109,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Event EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.EventHandler", "src\AWS.Lambda.Powertools.EventHandler\AWS.Lambda.Powertools.EventHandler.csproj", "{F4B8D5AF-D3CA-4910-A14D-E5BAEF0FD1DE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver", "src\AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver\AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj", "{281F7EB5-ACE5-458F-BC88-46A8899DF3BA}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction", "src\AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction\AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj", "{281F7EB5-ACE5-458F-BC88-46A8899DF3BA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj similarity index 71% rename from libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj rename to libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj index 87033029..1eacbf0f 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj @@ -2,10 +2,8 @@ - AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver + AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction Powertools for AWS Lambda (.NET) - Event Handler Bedrock Agent Function Resolver package. - AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver - AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver net8.0 false enable diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs similarity index 99% rename from libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs rename to libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs index bcf61e67..7bbcbcdd 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs @@ -5,7 +5,7 @@ using Amazon.Lambda.Core; // ReSharper disable once CheckNamespace -namespace AWS.Lambda.Powertools.EventHandler +namespace AWS.Lambda.Powertools.EventHandler.Resolvers { [JsonSerializable(typeof(string[]))] [JsonSerializable(typeof(int[]))] diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs similarity index 91% rename from libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs rename to libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs index 871e01b3..47ec0cfa 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs @@ -1,12 +1,12 @@ using Microsoft.Extensions.DependencyInjection; // ReSharper disable once CheckNamespace -namespace AWS.Lambda.Powertools.EventHandler +namespace AWS.Lambda.Powertools.EventHandler.Resolvers { /// /// Extended Bedrock Agent Function Resolver with dependency injection support. /// - public class DiBedrockAgentFunctionResolver : BedrockAgentFunctionResolver + internal class DiBedrockAgentFunctionResolver : BedrockAgentFunctionResolver { /// /// Gets the service provider used for dependency injection. diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/InternalsVisibleTo.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/InternalsVisibleTo.cs similarity index 100% rename from libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/InternalsVisibleTo.cs rename to libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/InternalsVisibleTo.cs diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/ParameterAccessor.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs similarity index 97% rename from libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/ParameterAccessor.cs rename to libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs index 51217357..eb614eb7 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/ParameterAccessor.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs @@ -1,7 +1,8 @@ using System.Globalization; using Amazon.BedrockAgentRuntime.Model; -namespace AWS.Lambda.Powertools.EventHandler; +// ReSharper disable once CheckNamespace +namespace AWS.Lambda.Powertools.EventHandler.Resolvers; /// /// Provides strongly-typed access to the parameters of an agent function call. diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md similarity index 100% rename from libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md rename to libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj index aab78861..1d0b8362 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj @@ -32,7 +32,7 @@ - + @@ -45,6 +45,10 @@ PreserveNewest + + + PreserveNewest + diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs index 0b0da0af..b0a85aad 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs @@ -9,23 +9,11 @@ #pragma warning disable CS0162 // Unreachable code detected -namespace AWS.Lambda.Powertools.EventHandler.Tests; +// ReSharper disable once CheckNamespace +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.Tests; public class BedrockAgentFunctionResolverTests { - private readonly ActionGroupInvocationInput _bedrockEvent; - - public BedrockAgentFunctionResolverTests() - { - _bedrockEvent = JsonSerializer.Deserialize( - File.ReadAllText("bedrockFunctionEvent.json"), - new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - Converters = { new JsonStringEnumConverter() } - })!; - } - [Fact] public void TestFunctionHandlerWithNoParameters() { @@ -725,6 +713,31 @@ public void TestFunctionHandlerWithStringArrayParameterManualParse() Assert.Contains("2. Push-ups, 3 sets of 10 reps", result.Text); Assert.Contains("3. Plank, 3 sets of 30 seconds", result.Text); } + + [Fact] + public async Task TestPayload2() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("getWeatherForCity", "Get weather for a specific city", async (string city, ILambdaContext context) => + { + return await Task.FromResult(city); + }); + + var input = JsonSerializer.Deserialize( + File.ReadAllText("bedrockFunctionEvent2.json"), + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() } + })!; + + // Act + var result = await resolver.ResolveAsync(input); + + // Assert + Assert.Equal("Lisbon", result.Text); + } [Fact] public void TestFunctionHandlerWithExceptionInHandler() diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json new file mode 100644 index 00000000..401be213 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json @@ -0,0 +1,27 @@ +{ + "messageVersion": "1.0", + "function": "get_weather_city", + "parameters": [ + { + "name": "month", + "type": "number", + "value": "5" + }, + { + "name": "city", + "type": "string", + "value": "London" + } + ], + "sessionId": "533568316194812", + "agent": { + "name": "powertools-function-agent", + "version": "DRAFT", + "id": "AVMWXZYN4X", + "alias": "TSTALIASID" + }, + "actionGroup": "action_group_quick_start_hgo6p", + "sessionAttributes": {}, + "promptSessionAttributes": {}, + "inputText": "weather in london?" +} \ No newline at end of file From e9ab64af788b9a9cf50c2d065f5471f2f67a8613 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 27 May 2025 17:05:09 +0100 Subject: [PATCH 07/52] add Bedrock function request and response models with agent support --- ...dler.Resolvers.BedrockAgentFunction.csproj | 2 +- .../BedrockAgentFunctionResolver.cs | 165 +++++---- .../Models/Agent.cs | 33 ++ .../Models/BedrockFunctionRequest.cs | 63 ++++ .../Models/BedrockFunctionResponse.cs | 64 ++++ .../Models/FunctionResponse.cs | 35 ++ .../Models/Parameter.cs | 29 ++ .../Models/Response.cs | 27 ++ .../Models/ResponseBody.cs | 15 + .../Models/TextBody.cs | 15 + .../ParameterAccessor.cs | 1 - .../BedrockAgentFunctionResolverTests.cs | 338 ++++++++++++++---- .../bedrockFunctionEvent2.json | 2 +- 13 files changed, 636 insertions(+), 153 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Agent.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionRequest.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionResponse.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/FunctionResponse.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Parameter.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Response.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/ResponseBody.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/TextBody.cs diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj index 1eacbf0f..1f5c2aea 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj @@ -9,11 +9,11 @@ enable enable true + 1.0.4 - diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs index 7bbcbcdd..072f5db5 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs @@ -1,8 +1,8 @@ using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; -using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; // ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler.Resolvers @@ -16,7 +16,7 @@ namespace AWS.Lambda.Powertools.EventHandler.Resolvers internal partial class BedrockFunctionResolverContext : JsonSerializerContext { } - + /// /// A resolver for Bedrock Agent functions that allows registering handlers for tool functions. /// @@ -27,7 +27,7 @@ internal partial class BedrockFunctionResolverContext : JsonSerializerContext /// resolver.Tool("GetWeather", (string city) => $"Weather in {city} is sunny"); /// /// // Lambda handler - /// public ActionGroupInvocationOutput FunctionHandler(ActionGroupInvocationInput input, ILambdaContext context) + /// public BedrockFunctionResponse FunctionHandler(BedrockFunctionRequest input, ILambdaContext context) /// { /// return resolver.Resolve(input, context); /// } @@ -36,7 +36,7 @@ internal partial class BedrockFunctionResolverContext : JsonSerializerContext public class BedrockAgentFunctionResolver { private readonly - Dictionary> + Dictionary> _handlers = new(); private static readonly HashSet _bedrockParameterTypes = new() @@ -58,11 +58,11 @@ private readonly }; private static bool IsBedrockParameter(Type type) => - _bedrockParameterTypes.Contains(type) || type.IsEnum || + _bedrockParameterTypes.Contains(type) || type.IsEnum || (type.IsArray && _bedrockParameterTypes.Contains(type.GetElementType()!)); /// - /// Registers a handler that directly accepts ActionGroupInvocationInput and returns ActionGroupInvocationOutput + /// Registers a handler that directly accepts BedrockFunctionRequest and returns BedrockFunctionResponse /// /// The name of the tool function /// The handler function that accepts input and context and returns output @@ -73,9 +73,9 @@ private static bool IsBedrockParameter(Type type) => /// var resolver = new BedrockAgentFunctionResolver(); /// resolver.Tool( /// "GetWeatherDetails", - /// (ActionGroupInvocationInput input, ILambdaContext context) => { + /// (BedrockFunctionRequest input, ILambdaContext context) => { /// context.Logger.LogLine($"Processing request for {input.Function}"); - /// return new ActionGroupInvocationOutput { Text = "Weather details response" }; + /// return new BedrockFunctionResponse { Text = "Weather details response" }; /// }, /// "Gets detailed weather information" /// ); @@ -83,7 +83,7 @@ private static bool IsBedrockParameter(Type type) => /// public BedrockAgentFunctionResolver Tool( string name, - Func handler, + Func handler, string description = "") { if (handler == null) @@ -94,7 +94,7 @@ public BedrockAgentFunctionResolver Tool( } /// - /// Registers a handler that directly accepts ActionGroupInvocationInput and returns ActionGroupInvocationOutput + /// Registers a handler that directly accepts BedrockFunctionRequest and returns BedrockFunctionResponse /// /// The name of the tool function /// The handler function that accepts input and returns output @@ -105,9 +105,9 @@ public BedrockAgentFunctionResolver Tool( /// var resolver = new BedrockAgentFunctionResolver(); /// resolver.Tool( /// "GetWeatherDetails", - /// (ActionGroupInvocationInput input) => { + /// (BedrockFunctionRequest input) => { /// var city = input.Parameters.FirstOrDefault(p => p.Name == "city")?.Value; - /// return new ActionGroupInvocationOutput { Text = $"Weather in {city} is sunny" }; + /// return new BedrockFunctionResponse { Text = $"Weather in {city} is sunny" }; /// }, /// "Gets weather for a city" /// ); @@ -115,7 +115,7 @@ public BedrockAgentFunctionResolver Tool( /// public BedrockAgentFunctionResolver Tool( string name, - Func handler, + Func handler, string description = "") { if (handler == null) @@ -126,7 +126,7 @@ public BedrockAgentFunctionResolver Tool( } /// - /// Registers a parameter-less handler that returns ActionGroupInvocationOutput + /// Registers a parameter-less handler that returns BedrockFunctionResponse /// /// The name of the tool function /// The handler function that returns output @@ -137,14 +137,14 @@ public BedrockAgentFunctionResolver Tool( /// var resolver = new BedrockAgentFunctionResolver(); /// resolver.Tool( /// "GetCurrentTime", - /// () => new ActionGroupInvocationOutput { Text = DateTime.Now.ToString() }, + /// () => new BedrockFunctionResponse { Text = DateTime.Now.ToString() }, /// "Gets the current server time" /// ); /// /// public BedrockAgentFunctionResolver Tool( string name, - Func handler, + Func handler, string description = "") { ArgumentNullException.ThrowIfNull(handler); @@ -177,7 +177,7 @@ public BedrockAgentFunctionResolver Tool( { ArgumentNullException.ThrowIfNull(handler); - _handlers[name] = (input, context) => new ActionGroupInvocationOutput { Text = handler() }; + _handlers[name] = (input, context) => BedrockFunctionResponse.WithText(handler(), input.ActionGroup, name); return this; } @@ -208,7 +208,7 @@ public BedrockAgentFunctionResolver Tool( _handlers[name] = (input, context) => { var result = handler(); - return ConvertToOutput(result); + return ConvertToOutput(result, input.ActionGroup, name); }; return this; } @@ -343,7 +343,7 @@ public BedrockAgentFunctionResolver Tool( { args[i] = context; } - else if (paramType == typeof(ActionGroupInvocationInput)) + else if (paramType == typeof(BedrockFunctionRequest)) { args[i] = input; } @@ -356,24 +356,30 @@ public BedrockAgentFunctionResolver Tool( if (paramType.IsArray) { var jsonArrayStr = accessor.Get(paramName); - + if (!string.IsNullOrEmpty(jsonArrayStr)) { try { // AOT-compatible deserialization using source generation if (paramType == typeof(string[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.StringArray); + args[i] = JsonSerializer.Deserialize(jsonArrayStr, + BedrockFunctionResolverContext.Default.StringArray); else if (paramType == typeof(int[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.Int32Array); + args[i] = JsonSerializer.Deserialize(jsonArrayStr, + BedrockFunctionResolverContext.Default.Int32Array); else if (paramType == typeof(long[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.Int64Array); + args[i] = JsonSerializer.Deserialize(jsonArrayStr, + BedrockFunctionResolverContext.Default.Int64Array); else if (paramType == typeof(double[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.DoubleArray); + args[i] = JsonSerializer.Deserialize(jsonArrayStr, + BedrockFunctionResolverContext.Default.DoubleArray); else if (paramType == typeof(bool[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.BooleanArray); + args[i] = JsonSerializer.Deserialize(jsonArrayStr, + BedrockFunctionResolverContext.Default.BooleanArray); else if (paramType == typeof(decimal[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.DecimalArray); + args[i] = JsonSerializer.Deserialize(jsonArrayStr, + BedrockFunctionResolverContext.Default.DecimalArray); else args[i] = null; // Unsupported array type } @@ -387,6 +393,7 @@ public BedrockAgentFunctionResolver Tool( args[i] = null; } } + if (paramType == typeof(string)) args[i] = accessor.Get(paramName); else if (paramType == typeof(int)) @@ -424,48 +431,47 @@ public BedrockAgentFunctionResolver Tool( // Execute the handler var result = handler.DynamicInvoke(args); - // Direct return for ActionGroupInvocationOutput - if (result is ActionGroupInvocationOutput output) + // Direct return for BedrockFunctionResponse + if (result is BedrockFunctionResponse output) return output; // Handle async results with specific type checks (AOT-compatible) - if (result is Task outputTask) + if (result is Task outputTask) return outputTask.Result; if (result is Task stringTask) - return ConvertToOutput((TResult)(object)stringTask.Result); + return ConvertToOutput((TResult)(object)stringTask.Result, input.ActionGroup, name); if (result is Task intTask) - return ConvertToOutput((TResult)(object)intTask.Result); + return ConvertToOutput((TResult)(object)intTask.Result, input.ActionGroup, name); if (result is Task boolTask) - return ConvertToOutput((TResult)(object)boolTask.Result); + return ConvertToOutput((TResult)(object)boolTask.Result, input.ActionGroup, name); if (result is Task doubleTask) - return ConvertToOutput((TResult)(object)doubleTask.Result); + return ConvertToOutput((TResult)(object)doubleTask.Result, input.ActionGroup, name); if (result is Task longTask) - return ConvertToOutput((TResult)(object)longTask.Result); + return ConvertToOutput((TResult)(object)longTask.Result, input.ActionGroup, name); if (result is Task decimalTask) - return ConvertToOutput((TResult)(object)decimalTask.Result); + return ConvertToOutput((TResult)(object)decimalTask.Result, input.ActionGroup, name); if (result is Task dateTimeTask) - return ConvertToOutput((TResult)(object)dateTimeTask.Result); + return ConvertToOutput((TResult)(object)dateTimeTask.Result, input.ActionGroup, name); if (result is Task guidTask) - return ConvertToOutput((TResult)(object)guidTask.Result); + return ConvertToOutput((TResult)(object)guidTask.Result, input.ActionGroup, name); if (result is Task objectTask) - return ConvertToOutput((TResult)objectTask.Result!); + return ConvertToOutput((TResult)objectTask.Result!, input.ActionGroup, name); // For regular Task with no result if (result is Task task) { task.GetAwaiter().GetResult(); - return new ActionGroupInvocationOutput { Text = string.Empty }; + return BedrockFunctionResponse.WithText(string.Empty, input.ActionGroup, name); } - return ConvertToOutput((TResult)result!); + return ConvertToOutput(result, input.ActionGroup, name); } catch (Exception ex) { - context?.Logger.LogError($"Error executing function {name}: {ex.Message}"); - return new ActionGroupInvocationOutput - { - Text = $"Error executing function: {ex.Message}" - }; + context?.Logger.LogError(ex.ToString()); + var innerException = ex.InnerException ?? ex; + return BedrockFunctionResponse.WithText($"Error executing function: {innerException.Message}", + input.ActionGroup, name); } }; @@ -481,7 +487,7 @@ public BedrockAgentFunctionResolver Tool( /// /// /// // Lambda handler - /// public ActionGroupInvocationOutput FunctionHandler(ActionGroupInvocationInput input, ILambdaContext context) + /// public BedrockFunctionResponse FunctionHandler(BedrockFunctionRequest input, ILambdaContext context) /// { /// var resolver = new BedrockAgentFunctionResolver() /// .Tool("GetWeather", (string city) => $"Weather in {city} is sunny") @@ -491,7 +497,7 @@ public BedrockAgentFunctionResolver Tool( /// } /// /// - public ActionGroupInvocationOutput Resolve(ActionGroupInvocationInput input, ILambdaContext? context = null) + public BedrockFunctionResponse Resolve(BedrockFunctionRequest input, ILambdaContext? context = null) { return ResolveAsync(input, context).GetAwaiter().GetResult(); } @@ -505,7 +511,7 @@ public ActionGroupInvocationOutput Resolve(ActionGroupInvocationInput input, ILa /// /// /// // Async Lambda handler - /// public async Task<ActionGroupInvocationOutput> FunctionHandler(ActionGroupInvocationInput input, ILambdaContext context) + /// public async Task<BedrockFunctionResponse> FunctionHandler(BedrockFunctionRequest input, ILambdaContext context) /// { /// var resolver = new BedrockAgentFunctionResolver() /// .Tool("GetWeatherAsync", async (string city) => { @@ -519,20 +525,17 @@ public ActionGroupInvocationOutput Resolve(ActionGroupInvocationInput input, ILa /// } /// /// - public async Task ResolveAsync(ActionGroupInvocationInput input, + public async Task ResolveAsync(BedrockFunctionRequest input, ILambdaContext? context = null) { return await Task.FromResult(HandleEvent(input, context)); } - private ActionGroupInvocationOutput HandleEvent(ActionGroupInvocationInput input, ILambdaContext? context) + private BedrockFunctionResponse HandleEvent(BedrockFunctionRequest input, ILambdaContext? context) { if (string.IsNullOrEmpty(input.Function)) { - return new ActionGroupInvocationOutput - { - Text = "No function specified in the request" - }; + return BedrockFunctionResponse.WithText("No function specified in the request", input.ActionGroup, ""); } if (_handlers.TryGetValue(input.Function, out var handler)) @@ -543,67 +546,77 @@ private ActionGroupInvocationOutput HandleEvent(ActionGroupInvocationInput input } catch (Exception ex) { - context?.Logger.LogError($"Error executing function {input.Function}: {ex.Message}"); - return new ActionGroupInvocationOutput - { - Text = $"Error executing function: {ex.Message}" - }; + context?.Logger.LogError(ex.ToString()); + return BedrockFunctionResponse.WithText($"Error executing function: {ex.Message}", input.ActionGroup, + input.Function); } } context?.Logger.LogWarning($"No handler registered for function: {input.Function}"); - return new ActionGroupInvocationOutput - { - Text = $"No handler registered for function: {input.Function}" - }; + return BedrockFunctionResponse.WithText($"No handler registered for function: {input.Function}", + input.ActionGroup, input.Function); } - private ActionGroupInvocationOutput ConvertToOutput(T result) + private BedrockFunctionResponse ConvertToOutput(T result, string actionGroup, string function) { if (result == null) { - return new ActionGroupInvocationOutput { Text = string.Empty }; + return BedrockFunctionResponse.WithText(string.Empty, actionGroup, function); } - // If result is already an ActionGroupInvocationOutput, return it directly - if (result is ActionGroupInvocationOutput output) + // If result is already an BedrockFunctionResponse, ensure action group and function are set + if (result is BedrockFunctionResponse output) { + // If the action group or function are not set in the output, use the provided values + if (string.IsNullOrEmpty(output.Response.ActionGroup)) + { + output.Response.ActionGroup = actionGroup; + } + + if (string.IsNullOrEmpty(output.Response.Function)) + { + output.Response.Function = function; + } + return output; } // For primitive types and strings, convert to string if (result is string str) { - return new ActionGroupInvocationOutput { Text = str }; + return BedrockFunctionResponse.WithText(str, actionGroup, function); } if (result is int intVal) { - return new ActionGroupInvocationOutput { Text = intVal.ToString(CultureInfo.InvariantCulture) }; + return BedrockFunctionResponse.WithText(intVal.ToString(CultureInfo.InvariantCulture), actionGroup, function); } if (result is double doubleVal) { - return new ActionGroupInvocationOutput { Text = doubleVal.ToString(CultureInfo.InvariantCulture) }; + return BedrockFunctionResponse.WithText(doubleVal.ToString(CultureInfo.InvariantCulture), actionGroup, + function); } if (result is bool boolVal) { - return new ActionGroupInvocationOutput { Text = boolVal.ToString() }; + return BedrockFunctionResponse.WithText(boolVal.ToString(), actionGroup, function); } if (result is long longVal) { - return new ActionGroupInvocationOutput { Text = longVal.ToString(CultureInfo.InvariantCulture) }; + return BedrockFunctionResponse.WithText(longVal.ToString(CultureInfo.InvariantCulture), actionGroup, + function); } if (result is decimal decimalVal) { - return new ActionGroupInvocationOutput { Text = decimalVal.ToString(CultureInfo.InvariantCulture) }; + return BedrockFunctionResponse.WithText(decimalVal.ToString(CultureInfo.InvariantCulture), actionGroup, + function); } - // For any other type, use ToString() instead of JSON serialization - return new ActionGroupInvocationOutput { Text = result.ToString() ?? string.Empty }; + // For any other type, use ToString() + return BedrockFunctionResponse.WithText(result.ToString() ?? string.Empty, actionGroup, function); } } -} +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Agent.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Agent.cs new file mode 100644 index 00000000..5e13b2fe --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Agent.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +/// +/// Represents an agent in the Bedrock Agent function input. +/// +public class Agent +{ + /// + /// Gets or sets the name of the agent. + /// + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the version of the agent. + /// + [JsonPropertyName("version")] + public string Version { get; set; } = string.Empty; + + /// + /// Gets or sets the ID of the agent. + /// + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// + /// Gets or sets the alias of the agent. + /// + [JsonPropertyName("alias")] + public string Alias { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionRequest.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionRequest.cs new file mode 100644 index 00000000..bd2d8395 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionRequest.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +/// +/// Represents the input for a Bedrock Agent function. +/// +public class BedrockFunctionRequest +{ + /// + /// Gets or sets the message version. + /// + [JsonPropertyName("messageVersion")] + public string MessageVersion { get; set; } = "1.0"; + + /// + /// Gets or sets the function name. + /// + [JsonPropertyName("function")] + public string Function { get; set; } = string.Empty; + + /// + /// Gets or sets the parameters for the function. + /// + [JsonPropertyName("parameters")] + public List Parameters { get; set; } = new List(); + + /// + /// Gets or sets the session ID. + /// + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// + /// Gets or sets the agent information. + /// + [JsonPropertyName("agent")] + public Agent? Agent { get; set; } + + /// + /// Gets or sets the action group. + /// + [JsonPropertyName("actionGroup")] + public string ActionGroup { get; set; } = string.Empty; + + /// + /// Gets or sets the session attributes. + /// + [JsonPropertyName("sessionAttributes")] + public Dictionary SessionAttributes { get; set; } = new Dictionary(); + + /// + /// Gets or sets the prompt session attributes. + /// + [JsonPropertyName("promptSessionAttributes")] + public Dictionary PromptSessionAttributes { get; set; } = new Dictionary(); + + /// + /// Gets or sets the input text. + /// + [JsonPropertyName("inputText")] + public string InputText { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionResponse.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionResponse.cs new file mode 100644 index 00000000..a86df718 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionResponse.cs @@ -0,0 +1,64 @@ +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +/// +/// The version of the message that identifies the format of the event data going into the Lambda function and the expected format of the response from a Lambda function. Amazon Bedrock only supports version 1.0. +/// +public class BedrockFunctionResponse +{ + /// + /// Gets or sets the message version. + /// + [JsonPropertyName("messageVersion")] + public string MessageVersion { get; } = "1.0"; + + /// + /// Gets or sets the response. + /// + [JsonPropertyName("response")] + public Response Response { get; set; } = new Response(); + + /// + /// Contains session attributes and their values. For more information, Session and prompt session attributes. + /// + [JsonPropertyName("sessionAttributes")] + public Dictionary SessionAttributes { get; set; } = new Dictionary(); + + /// + /// Contains prompt attributes and their values. For more information, Session and prompt session attributes. + /// + [JsonPropertyName("promptSessionAttributes")] + public Dictionary PromptSessionAttributes { get; set; } = new Dictionary(); + + /// + /// Contains a list of query configurations for knowledge bases attached to the agent. For more information, Knowledge base retrieval configurations. + /// + [JsonPropertyName("knowledgeBasesConfiguration")] + public Dictionary KnowledgeBasesConfiguration { get; set; } = new Dictionary(); + + + /// + /// Creates a new instance of BedrockFunctionResponse with the specified text. + /// + public static BedrockFunctionResponse WithText(string text, string actionGroup = "", string function = "") + { + return new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = actionGroup, + Function = function, + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = text } + } + } + }, + SessionAttributes = new Dictionary(), + PromptSessionAttributes = new Dictionary() + }; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/FunctionResponse.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/FunctionResponse.cs new file mode 100644 index 00000000..e22c97d6 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/FunctionResponse.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; +// ReSharper disable InconsistentNaming +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +/// +/// Represents the function response part of a Response. +/// +public class FunctionResponse +{ + /// + /// Contains an object that defines the response from execution of the function. The key is the content type (currently only TEXT is supported) and the value is an object containing the body of the response. + /// + [JsonPropertyName("responseBody")] + public ResponseBody ResponseBody { get; set; } = new ResponseBody(); + + /// + /// (Optional) – Set to one of the following states to define the agent's behavior after processing the action: + /// + /// FAILURE – The agent throws a DependencyFailedException for the current session. Applies when the function execution fails because of a dependency failure. + /// REPROMPT – The agent passes a response string to the model to reprompt it. Applies when the function execution fails because of invalid input. + /// + [JsonPropertyName("responseState")] + public ResponseState ResponseState { get; set; } +} + +/// +/// Represents the response state of a function response. +/// +public enum ResponseState +{ + FAILURE, + REPROMPT +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Parameter.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Parameter.cs new file mode 100644 index 00000000..5e5a65ee --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Parameter.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +// ReSharper disable once CheckNamespace +namespace AWS.Lambda.Powertools.EventHandler.Resolvers +{ + /// + /// Represents a parameter for a Bedrock Agent function. + /// + public class Parameter + { + /// + /// Gets or sets the name of the parameter. + /// + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the type of the parameter. + /// + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + /// + /// Gets or sets the value of the parameter. + /// + [JsonPropertyName("value")] + public string Value { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Response.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Response.cs new file mode 100644 index 00000000..5d2e76a7 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Response.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +/// +/// Represents the response part of an BedrockFunctionResponse. +/// +public class Response +{ + /// + /// Gets or sets the action group. + /// + [JsonPropertyName("actionGroup")] + public string ActionGroup { get; internal set; } = string.Empty; + + /// + /// Gets or sets the function. + /// + [JsonPropertyName("function")] + public string Function { get; internal set; } = string.Empty; + + /// + /// Gets or sets the function response. + /// + [JsonPropertyName("functionResponse")] + public FunctionResponse FunctionResponse { get; set; } = new FunctionResponse(); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/ResponseBody.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/ResponseBody.cs new file mode 100644 index 00000000..20bc59c2 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/ResponseBody.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +/// +/// Represents the response body part of a FunctionResponse. +/// +public class ResponseBody +{ + /// + /// Gets or sets the text body. + /// + [JsonPropertyName("TEXT")] + public TextBody Text { get; set; } = new TextBody(); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/TextBody.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/TextBody.cs new file mode 100644 index 00000000..8e9a41c7 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/TextBody.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +/// +/// Represents the text body part of a ResponseBody. +/// +public class TextBody +{ + /// + /// Gets or sets the body text. + /// + [JsonPropertyName("body")] + public string Body { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs index eb614eb7..bc8001db 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs @@ -1,5 +1,4 @@ using System.Globalization; -using Amazon.BedrockAgentRuntime.Model; // ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler.Resolvers; diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs index b0a85aad..28100fe3 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs @@ -2,9 +2,9 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; using Microsoft.Extensions.DependencyInjection; #pragma warning disable CS0162 // Unreachable code detected @@ -19,16 +19,30 @@ public void TestFunctionHandlerWithNoParameters() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + resolver.Tool("TestFunction", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello, World!" } + } + } + } + }); - var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var input = new BedrockFunctionRequest { Function = "TestFunction" }; var context = new TestLambdaContext(); // Act var result = resolver.Resolve(input, context); // Assert - Assert.Equal("Hello, World!", result.Text); + Assert.Equal("Hello, World!", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -36,16 +50,30 @@ public async Task TestFunctionHandlerWithNoParametersAsync() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + resolver.Tool("TestFunction", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello, World!" } + } + } + } + }); - var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var input = new BedrockFunctionRequest { Function = "TestFunction" }; var context = new TestLambdaContext(); // Act var result = await resolver.ResolveAsync(input, context); // Assert - Assert.Equal("Hello, World!", result.Text); + Assert.Equal("Hello, World!", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -53,17 +81,31 @@ public void TestFunctionHandlerWithDescription() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }, + resolver.Tool("TestFunction", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello, World!" } + } + } + } + }, "This is a test function"); - var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var input = new BedrockFunctionRequest { Function = "TestFunction" }; var context = new TestLambdaContext(); // Act var result = resolver.Resolve(input, context); // Assert - Assert.Equal("Hello, World!", result.Text); + Assert.Equal("Hello, World!", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -71,11 +113,40 @@ public void TestFunctionHandlerWithMultiplTools() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction1", () => new ActionGroupInvocationOutput { Text = "Hello from Function 1!" }); - resolver.Tool("TestFunction2", () => new ActionGroupInvocationOutput { Text = "Hello from Function 2!" }); + + resolver.Tool("TestFunction1", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello from Function 1!" } + } + } + } + }); + resolver.Tool("TestFunction2", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello from Function 2!" } + } + } + } + }); - var input1 = new ActionGroupInvocationInput { Function = "TestFunction1" }; - var input2 = new ActionGroupInvocationInput { Function = "TestFunction2" }; + var input1 = new BedrockFunctionRequest { Function = "TestFunction1" }; + var input2 = new BedrockFunctionRequest { Function = "TestFunction2" }; var context = new TestLambdaContext(); // Act @@ -83,8 +154,57 @@ public void TestFunctionHandlerWithMultiplTools() var result2 = resolver.Resolve(input2, context); // Assert - Assert.Equal("Hello from Function 1!", result1.Text); - Assert.Equal("Hello from Function 2!", result2.Text); + Assert.Equal("Hello from Function 1!", result1.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("Hello from Function 2!", result2.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void TestFunctionHandlerWithMultiplToolsDuplicate() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction1", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello from Function 1!" } + } + } + } + }); + resolver.Tool("TestFunction1", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello from Function 2!" } + } + } + } + }); + + var input1 = new BedrockFunctionRequest { Function = "TestFunction1" }; + var input2 = new BedrockFunctionRequest { Function = "TestFunction1" }; + var context = new TestLambdaContext(); + + // Act + var result1 = resolver.Resolve(input1, context); + var result2 = resolver.Resolve(input2, context); + + // Assert + Assert.Equal("Hello from Function 2!", result1.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("Hello from Function 2!", result2.Response.FunctionResponse.ResponseBody.Text.Body); } @@ -94,16 +214,30 @@ public void TestFunctionHandlerWithInput() // Arrange var resolver = new BedrockAgentFunctionResolver(); resolver.Tool("TestFunction", - (input, context) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); + (input, context) => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = $"Hello, {input.Function}!" } + } + } + } + }); - var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var input = new BedrockFunctionRequest { Function = "TestFunction" }; var context = new TestLambdaContext(); // Act var result = resolver.Resolve(input, context); // Assert - Assert.Equal("Hello, TestFunction!", result.Text); + Assert.Equal("Hello, TestFunction!", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -112,16 +246,30 @@ public async Task TestFunctionHandlerWithInputAsync() // Arrange var resolver = new BedrockAgentFunctionResolver(); resolver.Tool("TestFunction", - input => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); + input => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = $"Hello, {input.Function}!" } + } + } + } + }); - var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var input = new BedrockFunctionRequest { Function = "TestFunction" }; var context = new TestLambdaContext(); // Act var result = await resolver.ResolveAsync(input, context); // Assert - Assert.Equal("Hello, TestFunction!", result.Text); + Assert.Equal("Hello, TestFunction!", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -129,16 +277,31 @@ public void TestFunctionHandlerNoToolMatch() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + resolver.Tool("TestFunction", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello, World!" } + } + } + } + }); - var input = new ActionGroupInvocationInput { Function = "NonExistentFunction" }; + var input = new BedrockFunctionRequest { Function = "NonExistentFunction" }; var context = new TestLambdaContext(); // Act var result = resolver.Resolve(input, context); // Assert - Assert.Equal("No handler registered for function: NonExistentFunction", result.Text); + Assert.Equal("No handler registered for function: NonExistentFunction", + result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -146,16 +309,31 @@ public async Task TestFunctionHandlerNoToolMatchAsync() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + resolver.Tool("TestFunction", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello, World!" } + } + } + } + }); - var input = new ActionGroupInvocationInput { Function = "NonExistentFunction" }; + var input = new BedrockFunctionRequest { Function = "NonExistentFunction" }; var context = new TestLambdaContext(); // Act var result = await resolver.ResolveAsync(input, context); // Assert - Assert.Equal("No handler registered for function: NonExistentFunction", result.Text); + Assert.Equal("No handler registered for function: NonExistentFunction", + result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -163,9 +341,23 @@ public void TestFunctionHandlerWithParameters() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + resolver.Tool("TestFunction", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello, World!" } + } + } + } + }); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "TestFunction", Parameters = new List @@ -190,7 +382,7 @@ public void TestFunctionHandlerWithParameters() var result = resolver.Resolve(input, context); // Assert - Assert.Equal("Hello, World!", result.Text); + Assert.Equal("Hello, World!", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -220,7 +412,7 @@ public void TestFunctionHandlerWithEvent() handler: () => { return "Hello"; } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "GetCustomForecast", Parameters = new List @@ -246,7 +438,7 @@ public void TestFunctionHandlerWithEvent() var result = resolver.Resolve(input, context); // Assert - Assert.Equal("1-day forecast for Lisbon", result.Text); + Assert.Equal("1-day forecast for Lisbon", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -270,7 +462,7 @@ public void TestFunctionHandlerWithEventAndServices() } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "GetCustomForecast", Parameters = new List @@ -296,7 +488,7 @@ public void TestFunctionHandlerWithEventAndServices() var result = resolver.Resolve(input, context); // Assert - Assert.Equal("Forecast for Lisbon for 1 days", result.Text); + Assert.Equal("Forecast for Lisbon for 1 days", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -320,7 +512,7 @@ public void TestFunctionHandlerWithEventTypes() handler: (string name) => { return $"Hello {name}"; } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "GetCustomForecast", Parameters = new List @@ -346,7 +538,7 @@ public void TestFunctionHandlerWithEventTypes() var result = resolver.Resolve(input, context); // Assert - Assert.Equal("1-day forecast for Lisbon", result.Text); + Assert.Equal("1-day forecast for Lisbon", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -360,7 +552,7 @@ public void TestFunctionHandlerWithBooleanParameter() handler: (bool isEnabled) => { return $"Feature is {(isEnabled ? "enabled" : "disabled")}"; } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "TestBool", Parameters = new List @@ -378,7 +570,7 @@ public void TestFunctionHandlerWithBooleanParameter() var result = resolver.Resolve(input); // Assert - Assert.Equal("Feature is enabled", result.Text); + Assert.Equal("Feature is enabled", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -392,7 +584,7 @@ public void TestFunctionHandlerWithMissingRequiredParameter() handler: (string name) => $"Hello, {name}!" ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "RequiredParam", Parameters = new List() // Empty parameters @@ -402,7 +594,7 @@ public void TestFunctionHandlerWithMissingRequiredParameter() var result = resolver.Resolve(input); // Assert - Assert.Contains("Hello, !", result.Text); + Assert.Contains("Hello, !", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -419,7 +611,7 @@ public void TestFunctionHandlerWithMultipleParameterTypes() } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "ComplexFunction", Parameters = new List @@ -434,7 +626,7 @@ public void TestFunctionHandlerWithMultipleParameterTypes() var result = resolver.Resolve(input); // Assert - Assert.Equal("Name: Test, Count: 5, Active: True", result.Text); + Assert.Equal("Name: Test, Count: 5, Active: True", result.Response.FunctionResponse.ResponseBody.Text.Body); } public enum TestEnum @@ -455,7 +647,7 @@ public void TestFunctionHandlerWithEnumParameter() handler: (TestEnum option) => { return $"Selected option: {option}"; } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "EnumTest", Parameters = new List @@ -473,7 +665,7 @@ public void TestFunctionHandlerWithEnumParameter() var result = resolver.Resolve(input); // Assert - Assert.Equal("Selected option: Option2", result.Text); + Assert.Equal("Selected option: Option2", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -487,7 +679,7 @@ public void TestParameterNameCaseSensitivity() handler: (string userName) => $"Hello, {userName}!" ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "CaseTest", Parameters = new List @@ -505,7 +697,7 @@ public void TestParameterNameCaseSensitivity() var result = resolver.Resolve(input); // Assert - Assert.Equal("Hello, John!", result.Text); + Assert.Equal("Hello, John!", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -519,7 +711,7 @@ public void TestParameterOrderIndependence() handler: (string firstName, string lastName) => { return $"Name: {firstName} {lastName}"; } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "OrderTest", Parameters = new List @@ -534,7 +726,7 @@ public void TestParameterOrderIndependence() var result = resolver.Resolve(input); // Assert - Assert.Equal("Name: John Smith", result.Text); + Assert.Equal("Name: John Smith", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -552,7 +744,7 @@ public void TestFunctionHandlerWithDecimalParameter() } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "PriceCalculator", Parameters = new List @@ -570,7 +762,7 @@ public void TestFunctionHandlerWithDecimalParameter() var result = resolver.Resolve(input); // Assert - Assert.Contains("35.99", result.Text); + Assert.Contains("35.99", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -584,12 +776,12 @@ public void TestFunctionHandlerWithArrayParameter() handler: (string text) => { // In a real implementation, you'd parse the array from the string - // ActionGroupInvocationInput doesn't directly support array types + // BedrockFunctionRequest doesn't directly support array types return $"Received: {text}"; } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "ArrayTest", Parameters = new List @@ -607,7 +799,7 @@ public void TestFunctionHandlerWithArrayParameter() var result = resolver.Resolve(input); // Assert - Assert.Equal("Received: [\"item1\",\"item2\"]", result.Text); + Assert.Equal("Received: [\"item1\",\"item2\"]", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -632,7 +824,7 @@ public void TestFunctionHandlerWithStringArrayParameter() } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "ProcessWorkout", Parameters = new List @@ -651,10 +843,10 @@ public void TestFunctionHandlerWithStringArrayParameter() var result = resolver.Resolve(input); // Assert - Assert.Contains("Your workout plan:", result.Text); - Assert.Contains("1. Squats, 3 sets of 10 reps", result.Text); - Assert.Contains("2. Push-ups, 3 sets of 10 reps", result.Text); - Assert.Contains("3. Plank, 3 sets of 30 seconds", result.Text); + Assert.Contains("Your workout plan:", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Contains("1. Squats, 3 sets of 10 reps", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Contains("2. Push-ups, 3 sets of 10 reps", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Contains("3. Plank, 3 sets of 30 seconds", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -665,7 +857,7 @@ public void TestFunctionHandlerWithStringArrayParameterManualParse() resolver.Tool( name: "ProcessWorkout", description: "Process workout exercises", - handler: (ActionGroupInvocationInput input) => + handler: (BedrockFunctionRequest input) => { // Manual array parsing since the resolver doesn't natively support arrays var exercisesJson = input.Parameters.FirstOrDefault(p => p.Name == "exercises")?.Value ?? "[]"; @@ -689,7 +881,7 @@ public void TestFunctionHandlerWithStringArrayParameterManualParse() } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "ProcessWorkout", Parameters = new List @@ -708,23 +900,21 @@ public void TestFunctionHandlerWithStringArrayParameterManualParse() var result = resolver.Resolve(input); // Assert - Assert.Contains("Your workout plan:", result.Text); - Assert.Contains("1. Squats, 3 sets of 10 reps", result.Text); - Assert.Contains("2. Push-ups, 3 sets of 10 reps", result.Text); - Assert.Contains("3. Plank, 3 sets of 30 seconds", result.Text); + Assert.Contains("Your workout plan:", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Contains("1. Squats, 3 sets of 10 reps", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Contains("2. Push-ups, 3 sets of 10 reps", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Contains("3. Plank, 3 sets of 30 seconds", result.Response.FunctionResponse.ResponseBody.Text.Body); } - + [Fact] public async Task TestPayload2() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("getWeatherForCity", "Get weather for a specific city", async (string city, ILambdaContext context) => - { - return await Task.FromResult(city); - }); + resolver.Tool("get_weather_city", "Get weather for a specific city", + async (string city, ILambdaContext context) => { return await Task.FromResult(city); }); - var input = JsonSerializer.Deserialize( + var input = JsonSerializer.Deserialize( File.ReadAllText("bedrockFunctionEvent2.json"), new JsonSerializerOptions { @@ -736,7 +926,7 @@ public async Task TestPayload2() var result = await resolver.ResolveAsync(input); // Assert - Assert.Equal("Lisbon", result.Text); + Assert.Equal("Lisbon", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -754,13 +944,13 @@ public void TestFunctionHandlerWithExceptionInHandler() } ); - var input = new ActionGroupInvocationInput { Function = "ThrowingFunction" }; + var input = new BedrockFunctionRequest { Function = "ThrowingFunction" }; // Act var result = resolver.Resolve(input); // Assert - Assert.Contains("Error executing function", result.Text); + Assert.Contains("Error executing function", result.Response.FunctionResponse.ResponseBody.Text.Body); } } diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json index 401be213..18e21694 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json @@ -10,7 +10,7 @@ { "name": "city", "type": "string", - "value": "London" + "value": "Lisbon" } ], "sessionId": "533568316194812", From 1275752a6f2ec74157eee74750751ff66151db10 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 27 May 2025 19:14:39 +0100 Subject: [PATCH 08/52] enhance Bedrock function resolver with tool registration limits and session attribute handling --- .../BedrockAgentFunctionResolver.cs | 219 +++++++-- .../BedrockAgentFunctionResolverExtensions.cs | 15 + .../BedrockFunctionResolverContext.cs | 29 ++ .../Models/Agent.cs | 17 +- .../Models/BedrockFunctionRequest.cs | 34 +- .../Models/BedrockFunctionResponse.cs | 32 +- .../Models/FunctionResponse.cs | 19 +- .../Models/Parameter.cs | 15 + .../Models/Response.cs | 15 + .../Models/ResponseBody.cs | 15 + .../Models/TextBody.cs | 15 + .../ParameterAccessor.cs | 15 + .../BedrockAgentFunctionResolverTests.cs | 459 +++++++----------- 13 files changed, 551 insertions(+), 348 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockFunctionResolverContext.cs diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs index 072f5db5..d8e08bb0 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs @@ -1,22 +1,26 @@ -using System.Globalization; +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Globalization; using System.Text.Json; -using System.Text.Json.Serialization; using Amazon.Lambda.Core; using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; // ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler.Resolvers { - [JsonSerializable(typeof(string[]))] - [JsonSerializable(typeof(int[]))] - [JsonSerializable(typeof(long[]))] - [JsonSerializable(typeof(double[]))] - [JsonSerializable(typeof(bool[]))] - [JsonSerializable(typeof(decimal[]))] - internal partial class BedrockFunctionResolverContext : JsonSerializerContext - { - } - /// /// A resolver for Bedrock Agent functions that allows registering handlers for tool functions. /// @@ -35,6 +39,8 @@ internal partial class BedrockFunctionResolverContext : JsonSerializerContext /// public class BedrockAgentFunctionResolver { + private const int MaxTools = 5; + private readonly Dictionary> _handlers = new(); @@ -61,6 +67,28 @@ private static bool IsBedrockParameter(Type type) => _bedrockParameterTypes.Contains(type) || type.IsEnum || (type.IsArray && _bedrockParameterTypes.Contains(type.GetElementType()!)); + /// + /// Checks if another tool can be registered, and logs a warning if the maximum limit is reached + /// or if a tool with the same name is already registered + /// + /// The name of the tool being registered + /// True if the tool can be registered, false if the maximum limit is reached + private bool CanRegisterTool(string name) + { + if (_handlers.Count >= MaxTools && !_handlers.ContainsKey(name)) + { + Console.WriteLine($"WARNING: Maximum number of tools ({MaxTools}) reached. Tool '{name}' will not be registered."); + return false; + } + + if (_handlers.ContainsKey(name)) + { + Console.WriteLine($"WARNING: Tool {name} already registered. Overwriting with new definition."); + } + + return true; + } + /// /// Registers a handler that directly accepts BedrockFunctionRequest and returns BedrockFunctionResponse /// @@ -89,6 +117,9 @@ public BedrockAgentFunctionResolver Tool( if (handler == null) throw new ArgumentNullException(nameof(handler)); + if (!CanRegisterTool(name)) + return this; + _handlers[name] = handler; return this; } @@ -121,6 +152,9 @@ public BedrockAgentFunctionResolver Tool( if (handler == null) throw new ArgumentNullException(nameof(handler)); + if (!CanRegisterTool(name)) + return this; + _handlers[name] = (input, _) => handler(input); return this; } @@ -149,6 +183,9 @@ public BedrockAgentFunctionResolver Tool( { ArgumentNullException.ThrowIfNull(handler); + if (!CanRegisterTool(name)) + return this; + _handlers[name] = (input, context) => handler(); return this; } @@ -177,7 +214,16 @@ public BedrockAgentFunctionResolver Tool( { ArgumentNullException.ThrowIfNull(handler); - _handlers[name] = (input, context) => BedrockFunctionResponse.WithText(handler(), input.ActionGroup, name); + if (!CanRegisterTool(name)) + return this; + + _handlers[name] = (input, context) => BedrockFunctionResponse.WithText( + handler(), + input.ActionGroup, + name, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); return this; } @@ -205,10 +251,13 @@ public BedrockAgentFunctionResolver Tool( { ArgumentNullException.ThrowIfNull(handler); + if (!CanRegisterTool(name)) + return this; + _handlers[name] = (input, context) => { var result = handler(); - return ConvertToOutput(result, input.ActionGroup, name); + return ConvertToOutput(result, input); }; return this; } @@ -323,6 +372,9 @@ public BedrockAgentFunctionResolver Tool( if (handler == null) throw new ArgumentNullException(nameof(handler)); + if (!CanRegisterTool(name)) + return this; + _handlers[name] = (input, context) => { var accessor = new ParameterAccessor(input.Parameters); @@ -439,39 +491,50 @@ public BedrockAgentFunctionResolver Tool( if (result is Task outputTask) return outputTask.Result; if (result is Task stringTask) - return ConvertToOutput((TResult)(object)stringTask.Result, input.ActionGroup, name); + return ConvertToOutput((TResult)(object)stringTask.Result, input); if (result is Task intTask) - return ConvertToOutput((TResult)(object)intTask.Result, input.ActionGroup, name); + return ConvertToOutput((TResult)(object)intTask.Result, input); if (result is Task boolTask) - return ConvertToOutput((TResult)(object)boolTask.Result, input.ActionGroup, name); + return ConvertToOutput((TResult)(object)boolTask.Result, input); if (result is Task doubleTask) - return ConvertToOutput((TResult)(object)doubleTask.Result, input.ActionGroup, name); + return ConvertToOutput((TResult)(object)doubleTask.Result, input); if (result is Task longTask) - return ConvertToOutput((TResult)(object)longTask.Result, input.ActionGroup, name); + return ConvertToOutput((TResult)(object)longTask.Result, input); if (result is Task decimalTask) - return ConvertToOutput((TResult)(object)decimalTask.Result, input.ActionGroup, name); + return ConvertToOutput((TResult)(object)decimalTask.Result, input); if (result is Task dateTimeTask) - return ConvertToOutput((TResult)(object)dateTimeTask.Result, input.ActionGroup, name); + return ConvertToOutput((TResult)(object)dateTimeTask.Result, input); if (result is Task guidTask) - return ConvertToOutput((TResult)(object)guidTask.Result, input.ActionGroup, name); + return ConvertToOutput((TResult)(object)guidTask.Result, input); if (result is Task objectTask) - return ConvertToOutput((TResult)objectTask.Result!, input.ActionGroup, name); + return ConvertToOutput((TResult)objectTask.Result!, input); // For regular Task with no result if (result is Task task) { task.GetAwaiter().GetResult(); - return BedrockFunctionResponse.WithText(string.Empty, input.ActionGroup, name); + return BedrockFunctionResponse.WithText( + string.Empty, + input.ActionGroup, + name, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } - return ConvertToOutput(result, input.ActionGroup, name); + return ConvertToOutput(result, input); } catch (Exception ex) { context?.Logger.LogError(ex.ToString()); var innerException = ex.InnerException ?? ex; - return BedrockFunctionResponse.WithText($"Error executing function: {innerException.Message}", - input.ActionGroup, name); + return BedrockFunctionResponse.WithText( + $"Error when invoking tool: {innerException.Message}", + input.ActionGroup, + name, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } }; @@ -535,7 +598,13 @@ private BedrockFunctionResponse HandleEvent(BedrockFunctionRequest input, ILambd { if (string.IsNullOrEmpty(input.Function)) { - return BedrockFunctionResponse.WithText("No function specified in the request", input.ActionGroup, ""); + return BedrockFunctionResponse.WithText( + "No tool specified in the request", + input.ActionGroup, + "", + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } if (_handlers.TryGetValue(input.Function, out var handler)) @@ -547,21 +616,40 @@ private BedrockFunctionResponse HandleEvent(BedrockFunctionRequest input, ILambd catch (Exception ex) { context?.Logger.LogError(ex.ToString()); - return BedrockFunctionResponse.WithText($"Error executing function: {ex.Message}", input.ActionGroup, - input.Function); + return BedrockFunctionResponse.WithText( + $"Error when invoking tool: {ex.Message}", + input.ActionGroup, + input.Function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } } - context?.Logger.LogWarning($"No handler registered for function: {input.Function}"); - return BedrockFunctionResponse.WithText($"No handler registered for function: {input.Function}", - input.ActionGroup, input.Function); + context?.Logger.LogWarning($"Tool {input.Function} has not been registered."); + return BedrockFunctionResponse.WithText( + $"Error: Tool {input.Function} has not been registered in handler", + input.ActionGroup, + input.Function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } - private BedrockFunctionResponse ConvertToOutput(T result, string actionGroup, string function) + private BedrockFunctionResponse ConvertToOutput(T result, BedrockFunctionRequest input) { + string actionGroup = input.ActionGroup; + string function = input.Function; + if (result == null) { - return BedrockFunctionResponse.WithText(string.Empty, actionGroup, function); + return BedrockFunctionResponse.WithText( + string.Empty, + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } // If result is already an BedrockFunctionResponse, ensure action group and function are set @@ -584,39 +672,78 @@ private BedrockFunctionResponse ConvertToOutput(T result, string actionGroup, // For primitive types and strings, convert to string if (result is string str) { - return BedrockFunctionResponse.WithText(str, actionGroup, function); + return BedrockFunctionResponse.WithText( + str, + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } if (result is int intVal) { - return BedrockFunctionResponse.WithText(intVal.ToString(CultureInfo.InvariantCulture), actionGroup, function); + return BedrockFunctionResponse.WithText( + intVal.ToString(CultureInfo.InvariantCulture), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } if (result is double doubleVal) { - return BedrockFunctionResponse.WithText(doubleVal.ToString(CultureInfo.InvariantCulture), actionGroup, - function); + return BedrockFunctionResponse.WithText( + doubleVal.ToString(CultureInfo.InvariantCulture), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } if (result is bool boolVal) { - return BedrockFunctionResponse.WithText(boolVal.ToString(), actionGroup, function); + return BedrockFunctionResponse.WithText( + boolVal.ToString(), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } if (result is long longVal) { - return BedrockFunctionResponse.WithText(longVal.ToString(CultureInfo.InvariantCulture), actionGroup, - function); + return BedrockFunctionResponse.WithText( + longVal.ToString(CultureInfo.InvariantCulture), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } if (result is decimal decimalVal) { - return BedrockFunctionResponse.WithText(decimalVal.ToString(CultureInfo.InvariantCulture), actionGroup, - function); + return BedrockFunctionResponse.WithText( + decimalVal.ToString(CultureInfo.InvariantCulture), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } // For any other type, use ToString() - return BedrockFunctionResponse.WithText(result.ToString() ?? string.Empty, actionGroup, function); + return BedrockFunctionResponse.WithText( + result.ToString() ?? string.Empty, + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } } -} \ No newline at end of file +} diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs index 47ec0cfa..7887573d 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using Microsoft.Extensions.DependencyInjection; // ReSharper disable once CheckNamespace diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockFunctionResolverContext.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockFunctionResolverContext.cs new file mode 100644 index 00000000..f0f6e3fb --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockFunctionResolverContext.cs @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Text.Json.Serialization; + +// ReSharper disable once CheckNamespace +namespace AWS.Lambda.Powertools.EventHandler.Resolvers; + +[JsonSerializable(typeof(string[]))] +[JsonSerializable(typeof(int[]))] +[JsonSerializable(typeof(long[]))] +[JsonSerializable(typeof(double[]))] +[JsonSerializable(typeof(bool[]))] +[JsonSerializable(typeof(decimal[]))] +internal partial class BedrockFunctionResolverContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Agent.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Agent.cs index 5e13b2fe..05b5d20e 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Agent.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Agent.cs @@ -1,9 +1,24 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Text.Json.Serialization; namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; /// -/// Represents an agent in the Bedrock Agent function input. +/// Contains information about the name, ID, alias, and version of the agent that the action group belongs to. /// public class Agent { diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionRequest.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionRequest.cs index bd2d8395..0a9fa9ef 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionRequest.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionRequest.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Text.Json.Serialization; namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; @@ -8,55 +23,56 @@ namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Mode public class BedrockFunctionRequest { /// - /// Gets or sets the message version. + /// The version of the message that identifies the format of the event data going into the Lambda function and the expected format of the response from a Lambda function. Amazon Bedrock only supports version 1.0. /// [JsonPropertyName("messageVersion")] public string MessageVersion { get; set; } = "1.0"; /// - /// Gets or sets the function name. + /// The name of the function as defined in the function details for the action group. /// [JsonPropertyName("function")] public string Function { get; set; } = string.Empty; /// - /// Gets or sets the parameters for the function. + /// Contains a list of objects. Each object contains the name, type, and value of a parameter in the API operation, as defined in the OpenAPI schema, or in the function. /// [JsonPropertyName("parameters")] public List Parameters { get; set; } = new List(); /// - /// Gets or sets the session ID. + /// The unique identifier of the agent session. /// [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; /// - /// Gets or sets the agent information. + /// Contains information about the name, ID, alias, and version of the agent that the action group belongs to. /// [JsonPropertyName("agent")] public Agent? Agent { get; set; } /// - /// Gets or sets the action group. + /// The name of the action group. /// [JsonPropertyName("actionGroup")] public string ActionGroup { get; set; } = string.Empty; /// - /// Gets or sets the session attributes. + /// Contains session attributes and their values. These attributes are stored over a session and provide context for the agent. + /// For more information, see Session and prompt session attributes. /// [JsonPropertyName("sessionAttributes")] public Dictionary SessionAttributes { get; set; } = new Dictionary(); /// - /// Gets or sets the prompt session attributes. + /// Contains prompt session attributes and their values. These attributes are stored over a turn and provide context for the agent. /// [JsonPropertyName("promptSessionAttributes")] public Dictionary PromptSessionAttributes { get; set; } = new Dictionary(); /// - /// Gets or sets the input text. + /// The user input for the conversation turn. /// [JsonPropertyName("inputText")] public string InputText { get; set; } = string.Empty; diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionResponse.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionResponse.cs index a86df718..4d90dcba 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionResponse.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionResponse.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Text.Json.Serialization; namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; @@ -41,7 +56,13 @@ public class BedrockFunctionResponse /// /// Creates a new instance of BedrockFunctionResponse with the specified text. /// - public static BedrockFunctionResponse WithText(string text, string actionGroup = "", string function = "") + public static BedrockFunctionResponse WithText( + string? text, + string actionGroup = "", + string function = "", + Dictionary? sessionAttributes = null, + Dictionary? promptSessionAttributes = null, + Dictionary? knowledgeBasesConfiguration = null) { return new BedrockFunctionResponse { @@ -53,12 +74,13 @@ public static BedrockFunctionResponse WithText(string text, string actionGroup = { ResponseBody = new ResponseBody { - Text = new TextBody { Body = text } + Text = new TextBody { Body = text ?? string.Empty } } } }, - SessionAttributes = new Dictionary(), - PromptSessionAttributes = new Dictionary() + SessionAttributes = sessionAttributes ?? new Dictionary(), + PromptSessionAttributes = promptSessionAttributes ?? new Dictionary(), + KnowledgeBasesConfiguration = knowledgeBasesConfiguration ?? new Dictionary() }; } -} \ No newline at end of file +} diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/FunctionResponse.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/FunctionResponse.cs index e22c97d6..5366c902 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/FunctionResponse.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/FunctionResponse.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Text.Json.Serialization; // ReSharper disable InconsistentNaming #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -22,7 +37,9 @@ public class FunctionResponse /// REPROMPT – The agent passes a response string to the model to reprompt it. Applies when the function execution fails because of invalid input. /// [JsonPropertyName("responseState")] - public ResponseState ResponseState { get; set; } + [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ResponseState? ResponseState { get; set; } } /// diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Parameter.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Parameter.cs index 5e5a65ee..481eca67 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Parameter.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Parameter.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Text.Json.Serialization; // ReSharper disable once CheckNamespace diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Response.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Response.cs index 5d2e76a7..5d0720be 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Response.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Response.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Text.Json.Serialization; namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/ResponseBody.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/ResponseBody.cs index 20bc59c2..3081af44 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/ResponseBody.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/ResponseBody.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Text.Json.Serialization; namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/TextBody.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/TextBody.cs index 8e9a41c7..f3240a87 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/TextBody.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/TextBody.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Text.Json.Serialization; namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs index bc8001db..e81675ca 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Globalization; // ReSharper disable once CheckNamespace diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs index 28100fe3..f98cf6c2 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs @@ -45,37 +45,6 @@ public void TestFunctionHandlerWithNoParameters() Assert.Equal("Hello, World!", result.Response.FunctionResponse.ResponseBody.Text.Body); } - [Fact] - public async Task TestFunctionHandlerWithNoParametersAsync() - { - // Arrange - var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new BedrockFunctionResponse - { - Response = new Response - { - ActionGroup = "TestGroup", - Function = "TestFunction", - FunctionResponse = new FunctionResponse - { - ResponseBody = new ResponseBody - { - Text = new TextBody { Body = "Hello, World!" } - } - } - } - }); - - var input = new BedrockFunctionRequest { Function = "TestFunction" }; - var context = new TestLambdaContext(); - - // Act - var result = await resolver.ResolveAsync(input, context); - - // Assert - Assert.Equal("Hello, World!", result.Response.FunctionResponse.ResponseBody.Text.Body); - } - [Fact] public void TestFunctionHandlerWithDescription() { @@ -240,38 +209,6 @@ public void TestFunctionHandlerWithInput() Assert.Equal("Hello, TestFunction!", result.Response.FunctionResponse.ResponseBody.Text.Body); } - [Fact] - public async Task TestFunctionHandlerWithInputAsync() - { - // Arrange - var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", - input => new BedrockFunctionResponse - { - Response = new Response - { - ActionGroup = "TestGroup", - Function = "TestFunction", - FunctionResponse = new FunctionResponse - { - ResponseBody = new ResponseBody - { - Text = new TextBody { Body = $"Hello, {input.Function}!" } - } - } - } - }); - - var input = new BedrockFunctionRequest { Function = "TestFunction" }; - var context = new TestLambdaContext(); - - // Act - var result = await resolver.ResolveAsync(input, context); - - // Assert - Assert.Equal("Hello, TestFunction!", result.Response.FunctionResponse.ResponseBody.Text.Body); - } - [Fact] public void TestFunctionHandlerNoToolMatch() { @@ -300,91 +237,10 @@ public void TestFunctionHandlerNoToolMatch() var result = resolver.Resolve(input, context); // Assert - Assert.Equal("No handler registered for function: NonExistentFunction", + Assert.Equal($"Error: Tool {input.Function} has not been registered in handler", result.Response.FunctionResponse.ResponseBody.Text.Body); } - [Fact] - public async Task TestFunctionHandlerNoToolMatchAsync() - { - // Arrange - var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new BedrockFunctionResponse - { - Response = new Response - { - ActionGroup = "TestGroup", - Function = "TestFunction", - FunctionResponse = new FunctionResponse - { - ResponseBody = new ResponseBody - { - Text = new TextBody { Body = "Hello, World!" } - } - } - } - }); - - var input = new BedrockFunctionRequest { Function = "NonExistentFunction" }; - var context = new TestLambdaContext(); - - // Act - var result = await resolver.ResolveAsync(input, context); - - // Assert - Assert.Equal("No handler registered for function: NonExistentFunction", - result.Response.FunctionResponse.ResponseBody.Text.Body); - } - - [Fact] - public void TestFunctionHandlerWithParameters() - { - // Arrange - var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new BedrockFunctionResponse - { - Response = new Response - { - ActionGroup = "TestGroup", - Function = "TestFunction", - FunctionResponse = new FunctionResponse - { - ResponseBody = new ResponseBody - { - Text = new TextBody { Body = "Hello, World!" } - } - } - } - }); - - var input = new BedrockFunctionRequest - { - Function = "TestFunction", - Parameters = new List - { - new Parameter - { - Name = "a", - Value = "1", - Type = "Number" - }, - new Parameter - { - Name = "b", - Value = "1", - Type = "Number" - } - } - }; - var context = new TestLambdaContext(); - - // Act - var result = resolver.Resolve(input, context); - - // Assert - Assert.Equal("Hello, World!", result.Response.FunctionResponse.ResponseBody.Text.Body); - } - [Fact] public void TestFunctionHandlerWithEvent() { @@ -491,56 +347,6 @@ public void TestFunctionHandlerWithEventAndServices() Assert.Equal("Forecast for Lisbon for 1 days", result.Response.FunctionResponse.ResponseBody.Text.Body); } - [Fact] - public void TestFunctionHandlerWithEventTypes() - { - // Arrange - var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool( - name: "GetCustomForecast", - description: "Get detailed forecast for a location", - handler: (string location, int days, ILambdaContext ctx) => - { - ctx.Logger.LogLine($"Getting forecast for {location}"); - return $"{days}-day forecast for {location}"; - } - ); - - resolver.Tool( - name: "Greet", - description: "Greet a user", - handler: (string name) => { return $"Hello {name}"; } - ); - - var input = new BedrockFunctionRequest - { - Function = "GetCustomForecast", - Parameters = new List - { - new Parameter - { - Name = "location", - Value = "Lisbon", - Type = "String" - }, - new Parameter - { - Name = "days", - Value = "1", - Type = "Number" - } - } - }; - - var context = new TestLambdaContext(); - - // Act - var result = resolver.Resolve(input, context); - - // Assert - Assert.Equal("1-day forecast for Lisbon", result.Response.FunctionResponse.ResponseBody.Text.Body); - } - [Fact] public void TestFunctionHandlerWithBooleanParameter() { @@ -765,43 +571,6 @@ public void TestFunctionHandlerWithDecimalParameter() Assert.Contains("35.99", result.Response.FunctionResponse.ResponseBody.Text.Body); } - [Fact] - public void TestFunctionHandlerWithArrayParameter() - { - // Arrange - var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool( - name: "ArrayTest", - description: "Test with array parameter", - handler: (string text) => - { - // In a real implementation, you'd parse the array from the string - // BedrockFunctionRequest doesn't directly support array types - return $"Received: {text}"; - } - ); - - var input = new BedrockFunctionRequest - { - Function = "ArrayTest", - Parameters = new List - { - new Parameter - { - Name = "text", - Value = "[\"item1\",\"item2\"]", // Array as JSON string - Type = "Array" - } - } - }; - - // Act - var result = resolver.Resolve(input); - - // Assert - Assert.Equal("Received: [\"item1\",\"item2\"]", result.Response.FunctionResponse.ResponseBody.Text.Body); - } - [Fact] public void TestFunctionHandlerWithStringArrayParameter() { @@ -850,49 +619,57 @@ public void TestFunctionHandlerWithStringArrayParameter() } [Fact] - public void TestFunctionHandlerWithStringArrayParameterManualParse() + public void TestFunctionHandlerWithExceptionInHandler() { // Arrange var resolver = new BedrockAgentFunctionResolver(); resolver.Tool( - name: "ProcessWorkout", - description: "Process workout exercises", - handler: (BedrockFunctionRequest input) => + name: "ThrowingFunction", + description: "Function that throws exception", + handler: () => { - // Manual array parsing since the resolver doesn't natively support arrays - var exercisesJson = input.Parameters.FirstOrDefault(p => p.Name == "exercises")?.Value ?? "[]"; + throw new InvalidOperationException("Test error"); + return "This will not run:"; + } + ); - // Parse JSON array - var exercises = JsonSerializer.Deserialize(exercisesJson); + var input = new BedrockFunctionRequest { Function = "ThrowingFunction" }; - // Process the array items - var result = new StringBuilder(); - result.AppendLine("Your workout plan:"); + // Act + var result = resolver.Resolve(input); - if (exercises != null) - { - for (int i = 0; i < exercises.Length; i++) - { - result.AppendLine($" {i + 1}. {exercises[i]}"); - } - } + // Assert + Assert.Contains("Error when invoking tool: Test error", result.Response.FunctionResponse.ResponseBody.Text.Body); + } - return result.ToString(); - } + [Fact] + public void TestSessionAttributesPreservation() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "SessionTest", + description: "Test session attributes preservation", + handler: (string message) => message ); - var input = new BedrockFunctionRequest - { - Function = "ProcessWorkout", + var input = new BedrockFunctionRequest + { + Function = "SessionTest", + ActionGroup = "TestGroup", Parameters = new List { - new Parameter - { - Name = "exercises", - Value = - "[\"Squats, 3 sets of 10 reps\",\"Push-ups, 3 sets of 10 reps\",\"Plank, 3 sets of 30 seconds\"]", - Type = "String" // The type is still String even though it contains JSON - } + new Parameter { Name = "message", Value = "Hello", Type = "String" } + }, + SessionAttributes = new Dictionary + { + { "userId", "12345" }, + { "preferredLanguage", "en-US" } + }, + PromptSessionAttributes = new Dictionary + { + { "context", "customer_support" }, + { "previousQuestion", "How do I reset my password?" } } }; @@ -900,57 +677,167 @@ public void TestFunctionHandlerWithStringArrayParameterManualParse() var result = resolver.Resolve(input); // Assert - Assert.Contains("Your workout plan:", result.Response.FunctionResponse.ResponseBody.Text.Body); - Assert.Contains("1. Squats, 3 sets of 10 reps", result.Response.FunctionResponse.ResponseBody.Text.Body); - Assert.Contains("2. Push-ups, 3 sets of 10 reps", result.Response.FunctionResponse.ResponseBody.Text.Body); - Assert.Contains("3. Plank, 3 sets of 30 seconds", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("Hello", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal(2, result.SessionAttributes.Count); + Assert.Equal("12345", result.SessionAttributes["userId"]); + Assert.Equal("en-US", result.SessionAttributes["preferredLanguage"]); + Assert.Equal(2, result.PromptSessionAttributes.Count); + Assert.Equal("customer_support", result.PromptSessionAttributes["context"]); + Assert.Equal("How do I reset my password?", result.PromptSessionAttributes["previousQuestion"]); } [Fact] - public async Task TestPayload2() + public void TestSessionAttributesPreservationWithErrorHandling() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("get_weather_city", "Get weather for a specific city", - async (string city, ILambdaContext context) => { return await Task.FromResult(city); }); + resolver.Tool( + name: "ErrorTest", + description: "Test session attributes preservation with error", + handler: () => { throw new Exception("Test error"); return "This will not run"; } + ); - var input = JsonSerializer.Deserialize( - File.ReadAllText("bedrockFunctionEvent2.json"), - new JsonSerializerOptions + var input = new BedrockFunctionRequest + { + Function = "ErrorTest", + ActionGroup = "TestGroup", + SessionAttributes = new Dictionary + { + { "userId", "12345" }, + { "session", "active" } + }, + PromptSessionAttributes = new Dictionary { - PropertyNameCaseInsensitive = true, - Converters = { new JsonStringEnumConverter() } - })!; + { "lastAction", "login" } + } + }; // Act - var result = await resolver.ResolveAsync(input); + var result = resolver.Resolve(input); // Assert - Assert.Equal("Lisbon", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Contains("Error when invoking tool: Test error", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal(2, result.SessionAttributes.Count); + Assert.Equal("12345", result.SessionAttributes["userId"]); + Assert.Equal("active", result.SessionAttributes["session"]); + Assert.Equal(1, result.PromptSessionAttributes?.Count); + Assert.Equal("login", result.PromptSessionAttributes?["lastAction"]); } [Fact] - public void TestFunctionHandlerWithExceptionInHandler() + public void TestSessionAttributesPreservationWithNoToolMatch() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + + var input = new BedrockFunctionRequest + { + Function = "NonExistentTool", + SessionAttributes = new Dictionary + { + { "preferredTheme", "dark" } + }, + PromptSessionAttributes = new Dictionary + { + { "lastVisited", "homepage" } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains($"Error: Tool {input.Function} has not been registered in handler", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal(1, result.SessionAttributes?.Count); + Assert.Equal("dark", result.SessionAttributes?["preferredTheme"]); + Assert.Equal(1, result.PromptSessionAttributes?.Count); + Assert.Equal("homepage", result.PromptSessionAttributes?["lastVisited"]); + } + + [Fact] + public void TestSReturningNull() { // Arrange var resolver = new BedrockAgentFunctionResolver(); resolver.Tool( - name: "ThrowingFunction", - description: "Function that throws exception", + name: "NullTest", + description: "Test session attributes preservation with error", handler: () => { - throw new InvalidOperationException("Test error"); - return "This will not run:"; + string test = null!; + return test; } ); - - var input = new BedrockFunctionRequest { Function = "ThrowingFunction" }; + + var input = new BedrockFunctionRequest + { + Function = "NullTest", + }; // Act var result = resolver.Resolve(input); // Assert - Assert.Contains("Error executing function", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void TestMaximumToolLimit() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + + // Register 5 tools (the maximum) + for (int i = 1; i <= 5; i++) + { + var toolName = $"Tool{i}"; + var response = $"Response from {toolName}"; + resolver.Tool(toolName, () => response); + + // Verify each tool works as it's registered + var testInput = new BedrockFunctionRequest { Function = toolName }; + var testResult = resolver.Resolve(testInput); + Assert.Contains(response, testResult.Response.FunctionResponse.ResponseBody.Text.Body); + } + + // Try to register a 6th tool that should not be registered + resolver.Tool("Tool6", () => "This should not be registered"); + + // Verify the 6th tool doesn't work + var input6 = new BedrockFunctionRequest { Function = "Tool6" }; + var result6 = resolver.Resolve(input6); + + // 6th tool should not be registered + Assert.Contains("has not been registered", result6.Response.FunctionResponse.ResponseBody.Text.Body); + + // Double-check that the original 5 tools still work + for (int i = 1; i <= 5; i++) + { + var toolName = $"Tool{i}"; + var input = new BedrockFunctionRequest { Function = toolName }; + var result = resolver.Resolve(input); + Assert.Contains($"Response from {toolName}", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + } + + [Fact] + public void TestToolOverrideWithWarning() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + + // Register a tool + resolver.Tool("Calculator", () => "Original Calculator"); + + // Register same tool again with different implementation + resolver.Tool("Calculator", () => "New Calculator"); + + // Verify the tool was overridden + var input = new BedrockFunctionRequest { Function = "Calculator" }; + var result = resolver.Resolve(input); + + // The second registration should have overwritten the first + Assert.Equal("New Calculator", result.Response.FunctionResponse.ResponseBody.Text.Body); } } @@ -965,4 +852,4 @@ public async Task DoSomething(string location, int days) { return await Task.FromResult($"Forecast for {location} for {days} days"); } -} \ No newline at end of file +} From 7908a23dddf9d8e37dccf1971301d5b2fe0b88ec Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 27 May 2025 22:36:36 +0100 Subject: [PATCH 09/52] refactor BedrockAgentFunctionResolver to improve parameter type handling and streamline task result processing --- .../BedrockAgentFunctionResolver.cs | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs index d8e08bb0..c15bc13e 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs @@ -45,7 +45,7 @@ private readonly Dictionary> _handlers = new(); - private static readonly HashSet _bedrockParameterTypes = new() + private static readonly HashSet BedrockParameterTypes = new() { typeof(string), typeof(int), @@ -64,8 +64,8 @@ private readonly }; private static bool IsBedrockParameter(Type type) => - _bedrockParameterTypes.Contains(type) || type.IsEnum || - (type.IsArray && _bedrockParameterTypes.Contains(type.GetElementType()!)); + BedrockParameterTypes.Contains(type) || type.IsEnum || + (type.IsArray && BedrockParameterTypes.Contains(type.GetElementType()!)); /// /// Checks if another tool can be registered, and logs a warning if the maximum limit is reached @@ -507,22 +507,19 @@ public BedrockAgentFunctionResolver Tool( if (result is Task guidTask) return ConvertToOutput((TResult)(object)guidTask.Result, input); if (result is Task objectTask) - return ConvertToOutput((TResult)objectTask.Result!, input); + return ConvertToOutput((TResult)objectTask.Result, input); // For regular Task with no result - if (result is Task task) - { - task.GetAwaiter().GetResult(); - return BedrockFunctionResponse.WithText( - string.Empty, - input.ActionGroup, - name, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } + if (result is not Task task) return ConvertToOutput(result, input); + task.GetAwaiter().GetResult(); + return BedrockFunctionResponse.WithText( + string.Empty, + input.ActionGroup, + name, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); - return ConvertToOutput(result, input); } catch (Exception ex) { From aa01854356836e786611933ba03eefb30a4a6070 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 28 May 2025 09:45:48 +0100 Subject: [PATCH 10/52] refactor BedrockAgentFunctionResolver to remove tool registration limit and add attribute-based tool registration support --- .../BedrockAgentFunctionResolver.cs | 8 -- .../BedrockAgentFunctionResolverExtensions.cs | 98 ++++++++++++---- .../BedrockFunctionToolAttribute.cs | 60 ++++++++++ .../DiBedrockAgentFunctionResolver.cs | 21 ++++ .../BedrockAgentFunctionResolverTests.cs | 105 +++++++++++------- 5 files changed, 221 insertions(+), 71 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockFunctionToolAttribute.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/DiBedrockAgentFunctionResolver.cs diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs index c15bc13e..133db96c 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs @@ -39,8 +39,6 @@ namespace AWS.Lambda.Powertools.EventHandler.Resolvers /// public class BedrockAgentFunctionResolver { - private const int MaxTools = 5; - private readonly Dictionary> _handlers = new(); @@ -75,12 +73,6 @@ private static bool IsBedrockParameter(Type type) => /// True if the tool can be registered, false if the maximum limit is reached private bool CanRegisterTool(string name) { - if (_handlers.Count >= MaxTools && !_handlers.ContainsKey(name)) - { - Console.WriteLine($"WARNING: Maximum number of tools ({MaxTools}) reached. Tool '{name}' will not be registered."); - return false; - } - if (_handlers.ContainsKey(name)) { Console.WriteLine($"WARNING: Tool {name} already registered. Overwriting with new definition."); diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs index 7887573d..bd2c8cbc 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs @@ -13,31 +13,14 @@ * permissions and limitations under the License. */ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Reflection; using Microsoft.Extensions.DependencyInjection; // ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler.Resolvers { - /// - /// Extended Bedrock Agent Function Resolver with dependency injection support. - /// - internal class DiBedrockAgentFunctionResolver : BedrockAgentFunctionResolver - { - /// - /// Gets the service provider used for dependency injection. - /// - public IServiceProvider ServiceProvider { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The service provider for dependency injection. - public DiBedrockAgentFunctionResolver(IServiceProvider serviceProvider) - { - ServiceProvider = serviceProvider; - } - } - /// /// Extension methods for Bedrock Agent Function Resolver. /// @@ -48,11 +31,86 @@ public static class BedrockResolverExtensions /// /// The service collection to add the resolver to. /// The updated service collection. + /// + /// + /// public void ConfigureServices(IServiceCollection services) + /// { + /// services.AddBedrockResolver(); + /// + /// // Now you can inject BedrockAgentFunctionResolver into your services + /// } + /// + /// public static IServiceCollection AddBedrockResolver(this IServiceCollection services) { services.AddSingleton(sp => new DiBedrockAgentFunctionResolver(sp)); return services; } + + /// + /// Registers tools from a type marked with BedrockFunctionTypeAttribute. + /// + /// The type containing tool methods marked with BedrockFunctionToolAttribute + /// The resolver to register tools with + /// The resolver for method chaining + /// + /// + /// // Define your tool class + /// [BedrockFunctionType] + /// public class WeatherTools + /// { + /// [BedrockFunctionTool(Name = "GetWeather", Description = "Gets weather forecast")] + /// public static string GetWeather(string location, int days) + /// { + /// return $"Weather forecast for {location} for the next {days} days"; + /// } + /// } + /// + /// // Register the tools + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.RegisterTool<WeatherTools>(); + /// + /// + public static BedrockAgentFunctionResolver RegisterTool<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>( + this BedrockAgentFunctionResolver resolver) + where T : class + { + var type = typeof(T); + + // Check if class has the BedrockFunctionType attribute + if (!type.IsDefined(typeof(BedrockFunctionTypeAttribute), false)) + return resolver; + + // Look at all static methods with the tool attribute + foreach (var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public)) + { + var attr = method.GetCustomAttribute(); + if (attr == null) continue; + + string toolName = attr.Name ?? method.Name; + string description = attr.Description ?? + string.Empty; + + // Create delegate from the static method + var del = Delegate.CreateDelegate( + GetDelegateType(method), + method); + + // Call the Tool method directly instead of using reflection + resolver.Tool(toolName, description, del); + } + + return resolver; + } + + private static Type GetDelegateType(MethodInfo method) + { + var parameters = method.GetParameters(); + var parameterTypes = parameters.Select(p => p.ParameterType).ToList(); + parameterTypes.Add(method.ReturnType); + + return Expression.GetDelegateType(parameterTypes.ToArray()); + } } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockFunctionToolAttribute.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockFunctionToolAttribute.cs new file mode 100644 index 00000000..562e7804 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockFunctionToolAttribute.cs @@ -0,0 +1,60 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +// ReSharper disable once CheckNamespace +namespace AWS.Lambda.Powertools.EventHandler.Resolvers; + +/// +/// Marks a method as a Bedrock Agent function tool. +/// +/// +/// +/// [BedrockFunctionTool(Name = "GetWeather", Description = "Gets the weather for a location")] +/// public static string GetWeather(string location, int days) +/// { +/// return $"Weather forecast for {location} for the next {days} days"; +/// } +/// +/// +[AttributeUsage(AttributeTargets.Method)] +public class BedrockFunctionToolAttribute : Attribute +{ + /// + /// The name of the tool. If not specified, the method name will be used. + /// + public string? Name { get; set; } + + /// + /// The description of the tool. Used to provide context about the tool's functionality. + /// + public string? Description { get; set; } +} + +/// +/// Marks a class as containing Bedrock Agent function tools. +/// +/// +/// +/// [BedrockFunctionType] +/// public class WeatherTools +/// { +/// // Methods that can be registered as tools +/// } +/// +/// +[AttributeUsage(AttributeTargets.Class)] +public class BedrockFunctionTypeAttribute : Attribute +{ +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/DiBedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/DiBedrockAgentFunctionResolver.cs new file mode 100644 index 00000000..0d893623 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/DiBedrockAgentFunctionResolver.cs @@ -0,0 +1,21 @@ +namespace AWS.Lambda.Powertools.EventHandler.Resolvers; + +/// +/// Extended Bedrock Agent Function Resolver with dependency injection support. +/// +internal class DiBedrockAgentFunctionResolver : BedrockAgentFunctionResolver +{ + /// + /// Gets the service provider used for dependency injection. + /// + public IServiceProvider ServiceProvider { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The service provider for dependency injection. + public DiBedrockAgentFunctionResolver(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs index f98cf6c2..50b28fe6 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs @@ -1,7 +1,5 @@ using System.Globalization; using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; @@ -781,45 +779,6 @@ public void TestSReturningNull() Assert.Equal("", result.Response.FunctionResponse.ResponseBody.Text.Body); } - [Fact] - public void TestMaximumToolLimit() - { - // Arrange - var resolver = new BedrockAgentFunctionResolver(); - - // Register 5 tools (the maximum) - for (int i = 1; i <= 5; i++) - { - var toolName = $"Tool{i}"; - var response = $"Response from {toolName}"; - resolver.Tool(toolName, () => response); - - // Verify each tool works as it's registered - var testInput = new BedrockFunctionRequest { Function = toolName }; - var testResult = resolver.Resolve(testInput); - Assert.Contains(response, testResult.Response.FunctionResponse.ResponseBody.Text.Body); - } - - // Try to register a 6th tool that should not be registered - resolver.Tool("Tool6", () => "This should not be registered"); - - // Verify the 6th tool doesn't work - var input6 = new BedrockFunctionRequest { Function = "Tool6" }; - var result6 = resolver.Resolve(input6); - - // 6th tool should not be registered - Assert.Contains("has not been registered", result6.Response.FunctionResponse.ResponseBody.Text.Body); - - // Double-check that the original 5 tools still work - for (int i = 1; i <= 5; i++) - { - var toolName = $"Tool{i}"; - var input = new BedrockFunctionRequest { Function = toolName }; - var result = resolver.Resolve(input); - Assert.Contains($"Response from {toolName}", result.Response.FunctionResponse.ResponseBody.Text.Body); - } - } - [Fact] public void TestToolOverrideWithWarning() { @@ -839,14 +798,74 @@ public void TestToolOverrideWithWarning() // The second registration should have overwritten the first Assert.Equal("New Calculator", result.Response.FunctionResponse.ResponseBody.Text.Body); } + + [Fact] + public void TestAttributeBasedToolRegistration() + { + // Arrange + + var services = new ServiceCollection(); + services.AddSingleton(new MyImplementation()); + services.AddBedrockResolver(); + + var serviceProvider = services.BuildServiceProvider(); + var resolver = serviceProvider.GetRequiredService() + .RegisterTool(); + + // Create test input for echo function + var echoInput = new BedrockFunctionRequest + { + Function = "Echo", + Parameters = new List + { + new Parameter { Name = "message", Value = "Hello world", Type = "String" } + } + }; + + // Create test input for calculate function + var calcInput = new BedrockFunctionRequest + { + Function = "Calculate", + Parameters = new List + { + new Parameter { Name = "x", Value = "5", Type = "Number" }, + new Parameter { Name = "y", Value = "3", Type = "Number" } + } + }; + + // Act + var echoResult = resolver.Resolve(echoInput); + var calcResult = resolver.Resolve(calcInput); + + // Assert + Assert.Equal("You asked: Forecast for Lisbon for 1 days", echoResult.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("Result: 8", calcResult.Response.FunctionResponse.ResponseBody.Text.Body); + } + + // Example tool class using attributes + [BedrockFunctionType] + public class AttributeBasedTool + { + [BedrockFunctionTool(Name = "Echo", Description = "Echoes back the input message")] + public static string EchoMessage(string message, IMyInterface myInterface, ILambdaContext context) + { + return $"You asked: {myInterface.DoSomething("Lisbon", 1).Result}"; + } + + [BedrockFunctionTool(Name = "Calculate", Description = "Adds two numbers together")] + public static string Calculate(int x, int y) + { + return $"Result: {x + y}"; + } + } } -internal interface IMyInterface +public interface IMyInterface { Task DoSomething(string location, int days); } -internal class MyImplementation : IMyInterface +public class MyImplementation : IMyInterface { public async Task DoSomething(string location, int days) { From f8e0ed6ccd52839edd44d41098ce835f457d3b16 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 28 May 2025 20:42:44 +0100 Subject: [PATCH 11/52] add ASP.NET Core integration for Bedrock Agent Function Resolver with fluent API for function registration and automatic request processing --- libraries/AWS.Lambda.Powertools.sln | 15 ++ ...ers.BedrockAgentFunction.AspNetCore.csproj | 26 +++ .../BedrockFunctionRegistration.cs | 56 ++++++ .../BedrockMinimalApiExtensions.cs | 173 ++++++++++++++++++ .../Readme.md | 115 ++++++++++++ .../Readme.md | 65 +++++-- 6 files changed, 434 insertions(+), 16 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore.csproj create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/BedrockFunctionRegistration.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/BedrockMinimalApiExtensions.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/Readme.md diff --git a/libraries/AWS.Lambda.Powertools.sln b/libraries/AWS.Lambda.Powertools.sln index 056b3801..c3056d14 100644 --- a/libraries/AWS.Lambda.Powertools.sln +++ b/libraries/AWS.Lambda.Powertools.sln @@ -111,6 +111,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Event EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction", "src\AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction\AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj", "{281F7EB5-ACE5-458F-BC88-46A8899DF3BA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore", "src\AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore\AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore.csproj", "{8A22F22E-D10A-4897-A89A-DC76C267F6BB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -604,6 +606,18 @@ Global {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|x64.Build.0 = Release|Any CPU {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|x86.ActiveCfg = Release|Any CPU {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|x86.Build.0 = Release|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Debug|x64.Build.0 = Debug|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Debug|x86.Build.0 = Debug|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Release|Any CPU.Build.0 = Release|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Release|x64.ActiveCfg = Release|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Release|x64.Build.0 = Release|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Release|x86.ActiveCfg = Release|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution @@ -656,5 +670,6 @@ Global {61374D8E-F77C-4A31-AE07-35DAF1847369} = {1CFF5568-8486-475F-81F6-06105C437528} {F4B8D5AF-D3CA-4910-A14D-E5BAEF0FD1DE} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5} {281F7EB5-ACE5-458F-BC88-46A8899DF3BA} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5} + {8A22F22E-D10A-4897-A89A-DC76C267F6BB} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5} EndGlobalSection EndGlobal diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore.csproj b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore.csproj new file mode 100644 index 00000000..5e5c6666 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore.csproj @@ -0,0 +1,26 @@ + + + + + AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore + Powertools for AWS Lambda (.NET) - Event Handler Bedrock Agent Function Resolver AspNetCore package. + AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore + AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore + net8.0 + false + enable + enable + + + + + + + + + + + + + + diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/BedrockFunctionRegistration.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/BedrockFunctionRegistration.cs new file mode 100644 index 00000000..9004a49d --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/BedrockFunctionRegistration.cs @@ -0,0 +1,56 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore; + +/// +/// Helper class for function registration with fluent API pattern. +/// +internal class BedrockFunctionRegistration +{ + private readonly BedrockAgentFunctionResolver _resolver; + + /// + /// Initializes a new instance of the class. + /// + /// The Bedrock agent function resolver. + public BedrockFunctionRegistration(BedrockAgentFunctionResolver resolver) + { + _resolver = resolver; + } + + /// + /// Adds a function to the Bedrock resolver. + /// + /// The name of the function. + /// The delegate handler. + /// Optional description of the function. + /// The function registration instance for method chaining. + /// + /// + /// app.MapBedrockFunction("GetWeather", (string city, int month) => + /// $"Weather forecast for {city} in month {month}: Warm and sunny"); + /// + /// app.MapBedrockFunction("Calculate", (int x, int y) => + /// $"Result: {x + y}"); + /// ); + /// + /// + public BedrockFunctionRegistration Add(string name, Delegate handler, string description = "") + { + _resolver.Tool(name, description, handler); + return this; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/BedrockMinimalApiExtensions.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/BedrockMinimalApiExtensions.cs new file mode 100644 index 00000000..9cbe0ae1 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/BedrockMinimalApiExtensions.cs @@ -0,0 +1,173 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore; + +// Source generation for JSON serialization +[JsonSerializable(typeof(BedrockFunctionRequest))] +internal partial class BedrockJsonContext : JsonSerializerContext +{ +} + +/// +/// Extension methods for registering Bedrock Agent Functions in ASP.NET Core Minimal API. +/// +public static class BedrockMinimalApiExtensions +{ + // Static flag to track if handler is mapped (thread-safe with volatile) + private static volatile bool _bedrockRequestHandlerMapped; + + // JSON options with case insensitivity + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + /// + /// Maps an individual Bedrock Agent function that will be called directly from the root endpoint. + /// The function name is extracted from the incoming request payload. + /// + /// The web application to configure. + /// The name of the function to register. + /// The delegate handler that implements the function. + /// Optional description of the function. + /// The web application instance. + /// + /// + /// // Register individual functions + /// app.MapBedrockFunction("GetWeather", (string city, int month) => + /// $"Weather forecast for {city} in month {month}: Warm and sunny"); + /// + /// app.MapBedrockFunction("Calculate", (int x, int y) => + /// $"Result: {x + y}"); + /// + /// + public static WebApplication MapBedrockFunction( + this WebApplication app, + string functionName, + Delegate handler, + string description = "") + { + // Get or create the resolver from services + var resolver = app.Services.GetService() + ?? new BedrockAgentFunctionResolver(); + + // Register the function with the resolver + resolver.Tool(functionName, description, handler); + + // Ensure we have a global handler for Bedrock requests + EnsureBedrockRequestHandler(app, resolver); + + return app; + } + + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", + Justification = "The handler implementation is controlled and AOT-compatible")] + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The handler implementation is controlled and trim-compatible")] + private static void EnsureBedrockRequestHandler(WebApplication app, BedrockAgentFunctionResolver resolver) + { + // Check if we've already mapped the handler (we only need to do this once) + if (_bedrockRequestHandlerMapped) + return; + + // Map the root endpoint to handle all Bedrock Agent Function requests + app.MapPost("/", [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Handler is AOT-friendly")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Handler is trim-friendly")] + async (HttpContext context) => + { + try + { + // Read the request body + string requestBody; + using (var reader = new StreamReader(context.Request.Body)) + { + requestBody = await reader.ReadToEndAsync(); + } + + // Use source-generated serialization for the request + var bedrockRequest = JsonSerializer.Deserialize(requestBody, + BedrockJsonContext.Default.BedrockFunctionRequest); + + if (bedrockRequest == null) + return Results.BadRequest("Invalid request format"); + + // Process the request through the resolver + var result = await resolver.ResolveAsync(bedrockRequest); + + // For the response, use the standard serializer with suppressed warnings + // This is more compatible with different response types + context.Response.ContentType = "application/json"; + await context.Response.WriteAsJsonAsync(result, JsonOptions); + return Results.Empty; + } + catch (Exception ex) + { + return Results.Problem($"Error processing Bedrock Agent request: {ex.Message}"); + } + }); + + // Mark that we've set up the handler + _bedrockRequestHandlerMapped = true; + } + + /// + /// Registers all methods from a class marked with BedrockFunctionTypeAttribute. + /// + /// The type containing tool methods marked with BedrockFunctionToolAttribute + /// The web application to configure. + /// The web application instance. + /// + /// + /// // Define your tool class + /// [BedrockFunctionType] + /// public class WeatherTools + /// { + /// [BedrockFunctionTool(Name = "GetWeather", Description = "Gets weather forecast")] + /// public static string GetWeather(string location, int days) + /// { + /// return $"Weather forecast for {location} for the next {days} days"; + /// } + /// } + /// + /// // Register all tools from the class + /// app.MapBedrockToolClass<WeatherTools>(); + /// + /// + public static WebApplication MapBedrockToolType<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>( + this WebApplication app) + where T : class + { + // Get or create the resolver from services + var resolver = app.Services.GetService() + ?? new BedrockAgentFunctionResolver(); + + // Register the tool class + resolver.RegisterTool(); + + // Ensure we have a global handler for Bedrock requests + EnsureBedrockRequestHandler(app, resolver); + + return app; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/Readme.md b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/Readme.md new file mode 100644 index 00000000..8cc31365 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/Readme.md @@ -0,0 +1,115 @@ +# Experimental work in progress, not yet released + +# AWS Lambda Powertools for .NET - Bedrock Agent Function Resolver for ASP.NET Core + +## Overview +This library provides ASP.NET Core integration for the AWS Lambda Powertools Bedrock Agent Function Resolver. It enables you to easily expose Bedrock Agent functions as endpoints in your ASP.NET Core applications using a simple, fluent API. + +## Features + +- **Minimal API Integration**: Register Bedrock Agent functions using familiar ASP.NET Core Minimal API patterns +- **AOT Compatibility**: Full support for .NET 8 AOT compilation through source generation +- **Simple Function Registration**: Register functions with a fluent API +- **Automatic Request Processing**: Automatic parsing of Bedrock Agent requests and formatting of responses +- **Error Handling**: Built-in error handling for Bedrock Agent function requests + +## Installation + +Install the package via NuGet: + +```bash +dotnet add package AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore +``` + +## Basic Usage + +Here's how to register Bedrock Agent functions in your ASP.NET Core application: + +```csharp +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +// Register individual functions +app.MapBedrockFunction("GetWeather", (string city, int month) => + $"Weather forecast for {city} in month {month}: Warm and sunny"); + +app.MapBedrockFunction("Calculate", (int x, int y) => + $"Result: {x + y}"); + +app.Run(); +``` + +When Amazon Bedrock Agent sends a request to your application, the appropriate function will be invoked with the extracted parameters, and the response will be formatted correctly for the agent. + +## Using with Dependency Injection + +Register the Bedrock resolver with dependency injection for more advanced scenarios: + +```csharp +using AWS.Lambda.Powertools.EventHandler.Resolvers; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); + +// Register the resolver and any other services +builder.Services.AddBedrockResolver(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +// Register functions that use injected services +app.MapBedrockFunction("GetWeatherForecast", + (string city, IWeatherService weatherService) => + weatherService.GetForecast(city), + "Gets weather forecast for a city"); + +app.Run(); +``` + +## Advanced Usage + +### Function Documentation + +Add descriptions to your functions for better documentation: + +```csharp +app.MapBedrockFunction("GetWeather", + (string city, int month) => $"Weather forecast for {city} in month {month}: Warm and sunny", + "Gets weather forecast for a specific city and month"); +``` + +### Working with Tool Classes + +Use the `MapBedrockToolClass()` method to register all functions from a class directly: + +```csharp +[BedrockFunctionType] +public class WeatherTools +{ + [BedrockFunctionTool(Name = "GetWeather", Description = "Gets weather forecast")] + public static string GetWeather(string location, int days) + { + return $"Weather forecast for {location} for the next {days} days"; + } +} + +// In Program.cs - directly register the tool class +app.MapBedrockToolClass(); +``` + +## How It Works + +1. When you call `MapBedrockFunction`, the function is registered with the resolver +2. An HTTP endpoint is set up at the root path (/) to handle incoming Bedrock Agent requests +3. When a request arrives, the library: + - Deserializes the JSON payload + - Extracts the function name and parameters + - Invokes the matching function with the appropriate parameters + - Serializes the result and returns it as a response + +## Requirements + +- .NET 8.0 or later +- ASP.NET Core 8.0 or later \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md index decd8abb..d1017d76 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md @@ -204,24 +204,61 @@ resolver.Tool( ## Supported Parameter Types - `string` -- `int` / `long` -- `double` / `decimal` +- `int` +- `number` - `bool` -- `DateTime` -- `Guid` - `enum` types - `ILambdaContext` (for accessing Lambda context) - `ActionGroupInvocationInput` (for accessing raw request) - Any service registered in dependency injection -## Benefits -- **Reduced Boilerplate**: Eliminate repetitive code for parsing requests and formatting responses -- **Type Safety**: Strong typing for parameters and return values -- **Simplified Development**: Focus on business logic instead of request/response handling -- **Reusable Components**: Build a library of tool functions that can be shared across agents -- **Easy Testing**: Functions can be easily unit tested in isolation -- **Flexible Integration**: Works seamlessly with AWS Lambda and Bedrock Agents +## 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 + +```csharp +// 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: + +```csharp + +var services = new ServiceCollection(); +services.AddSingleton(); +services.AddBedrockResolver(); // Extension method to register the resolver + +var serviceProvider = services.BuildServiceProvider(); +var resolver = serviceProvider.GetRequiredService() + .RegisterTool(); // Register tools from the class during service registration + +``` ## Complete Example with Dependency Injection @@ -297,8 +334,4 @@ namespace MyBedrockAgent } } } -``` - -## Learn More - -For more information about Amazon Bedrock Agents and function integration, see the [Amazon Bedrock documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-tools.html). +``` \ No newline at end of file From b0af769f0d96b2797d5f8f6593ff6626822c81ef Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 28 May 2025 20:44:18 +0100 Subject: [PATCH 12/52] bump BedrockAgentFunctionResolver version to 1.0.0-alpha.1 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 759ed8a3..cb5c98bc 100644 --- a/version.json +++ b/version.json @@ -10,6 +10,6 @@ "Idempotency": "1.3.0", "BatchProcessing": "1.2.1", "EventHandler": "1.0.0", - "BedrockAgentFunctionResolver": "1.0.0" + "BedrockAgentFunctionResolver": "1.0.0-alpha.1", } } From 3380df78d2259247751fbc3991c7f343baa52480 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 May 2025 09:32:30 +0100 Subject: [PATCH 13/52] refactor BedrockAgentFunctionResolver to enhance parameter mapping and result processing with dedicated helper classes --- .../BedrockAgentFunctionResolver.cs | 479 ++---------------- .../Helpers/ParameterMapper.cs | 150 ++++++ .../Helpers/ParameterTypeValidator.cs | 50 ++ .../Helpers/ResultConverter.cs | 238 +++++++++ 4 files changed, 493 insertions(+), 424 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterTypeValidator.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs index 133db96c..a4ece182 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs @@ -17,6 +17,7 @@ using System.Text.Json; using Amazon.Lambda.Core; using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers; // ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler.Resolvers @@ -43,27 +44,9 @@ private readonly Dictionary> _handlers = new(); - private static readonly HashSet BedrockParameterTypes = new() - { - typeof(string), - typeof(int), - typeof(long), - typeof(double), - typeof(bool), - typeof(decimal), - typeof(DateTime), - typeof(Guid), - typeof(string[]), - typeof(int[]), - typeof(long[]), - typeof(double[]), - typeof(bool[]), - typeof(decimal[]) - }; - - private static bool IsBedrockParameter(Type type) => - BedrockParameterTypes.Contains(type) || type.IsEnum || - (type.IsArray && BedrockParameterTypes.Contains(type.GetElementType()!)); + private readonly ParameterTypeValidator _parameterValidator = new(); + private readonly ResultConverter _resultConverter = new(); + private readonly ParameterMapper _parameterMapper = new(); /// /// Checks if another tool can be registered, and logs a warning if the maximum limit is reached @@ -88,26 +71,12 @@ private bool CanRegisterTool(string name) /// The handler function that accepts input and context and returns output /// Optional description of the tool function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// resolver.Tool( - /// "GetWeatherDetails", - /// (BedrockFunctionRequest input, ILambdaContext context) => { - /// context.Logger.LogLine($"Processing request for {input.Function}"); - /// return new BedrockFunctionResponse { Text = "Weather details response" }; - /// }, - /// "Gets detailed weather information" - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, Func handler, string description = "") { - if (handler == null) - throw new ArgumentNullException(nameof(handler)); + ArgumentNullException.ThrowIfNull(handler); if (!CanRegisterTool(name)) return this; @@ -123,26 +92,12 @@ public BedrockAgentFunctionResolver Tool( /// The handler function that accepts input and returns output /// Optional description of the tool function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// resolver.Tool( - /// "GetWeatherDetails", - /// (BedrockFunctionRequest input) => { - /// var city = input.Parameters.FirstOrDefault(p => p.Name == "city")?.Value; - /// return new BedrockFunctionResponse { Text = $"Weather in {city} is sunny" }; - /// }, - /// "Gets weather for a city" - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, Func handler, string description = "") { - if (handler == null) - throw new ArgumentNullException(nameof(handler)); + ArgumentNullException.ThrowIfNull(handler); if (!CanRegisterTool(name)) return this; @@ -158,16 +113,6 @@ public BedrockAgentFunctionResolver Tool( /// The handler function that returns output /// Optional description of the tool function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// resolver.Tool( - /// "GetCurrentTime", - /// () => new BedrockFunctionResponse { Text = DateTime.Now.ToString() }, - /// "Gets the current server time" - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, Func handler, @@ -178,7 +123,7 @@ public BedrockAgentFunctionResolver Tool( if (!CanRegisterTool(name)) return this; - _handlers[name] = (input, context) => handler(); + _handlers[name] = (_, _) => handler(); return this; } @@ -189,16 +134,6 @@ public BedrockAgentFunctionResolver Tool( /// The handler function that returns a string /// Optional description of the tool function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// resolver.Tool( - /// "GetGreeting", - /// () => "Hello, world!", - /// "Returns a greeting message" - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, Func handler, @@ -209,7 +144,7 @@ public BedrockAgentFunctionResolver Tool( if (!CanRegisterTool(name)) return this; - _handlers[name] = (input, context) => BedrockFunctionResponse.WithText( + _handlers[name] = (input, _) => BedrockFunctionResponse.WithText( handler(), input.ActionGroup, name, @@ -226,16 +161,6 @@ public BedrockAgentFunctionResolver Tool( /// The handler function that returns an object /// Optional description of the tool function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// resolver.Tool( - /// "GetServerStatus", - /// () => new { Status = "Online", Uptime = "99.9%" }, - /// "Returns the server status information" - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, Func handler, @@ -246,10 +171,10 @@ public BedrockAgentFunctionResolver Tool( if (!CanRegisterTool(name)) return this; - _handlers[name] = (input, context) => + _handlers[name] = (input, _) => { var result = handler(); - return ConvertToOutput(result, input); + return _resultConverter.ConvertToOutput(result, input); }; return this; } @@ -260,15 +185,6 @@ public BedrockAgentFunctionResolver Tool( /// The name of the tool function /// The delegate handler function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// resolver.Tool( - /// "CalculateSum", - /// (int a, int b) => a + b - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, Delegate handler) @@ -283,16 +199,6 @@ public BedrockAgentFunctionResolver Tool( /// Description of the tool function /// The delegate handler function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// resolver.Tool( - /// "GetWeather", - /// "Gets the weather forecast for a specific city", - /// (string city, int days) => $"{days}-day forecast for {city}: Sunny" - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, string description, @@ -308,15 +214,6 @@ public BedrockAgentFunctionResolver Tool( /// The name of the tool function /// The delegate handler function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// resolver.Tool<int>( - /// "CalculateArea", - /// (int width, int height) => width * height - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, Delegate handler) @@ -332,186 +229,33 @@ public BedrockAgentFunctionResolver Tool( /// Description of the tool function /// The delegate handler function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// - /// // Register a function with strongly typed parameters and return value - /// resolver.Tool<double>( - /// "CalculateDistance", - /// "Calculates the distance between two points", - /// (double x1, double y1, double x2, double y2) => { - /// return Math.Sqrt(Math.Pow(x2 - x1, 2) + Math.Pow(y2 - y1, 2)); - /// } - /// ); - /// - /// // Register a function that accepts Lambda context - /// resolver.Tool<string>( - /// "LogAndReturn", - /// "Logs a message and returns it", - /// (string message, ILambdaContext context) => { - /// context.Logger.LogLine($"Message received: {message}"); - /// return message; - /// } - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, string description, Delegate handler) { - if (handler == null) - throw new ArgumentNullException(nameof(handler)); + ArgumentNullException.ThrowIfNull(handler); if (!CanRegisterTool(name)) return this; - _handlers[name] = (input, context) => - { - var accessor = new ParameterAccessor(input.Parameters); - var parameters = handler.Method.GetParameters(); - var args = new object?[parameters.Length]; - var bedrockParamIndex = 0; - - // Get service provider from resolver if available - var serviceProvider = (this as DiBedrockAgentFunctionResolver)?.ServiceProvider; - - // Map parameters from Bedrock input and DI - for (var i = 0; i < parameters.Length; i++) - { - var parameter = parameters[i]; - var paramType = parameter.ParameterType; - - if (paramType == typeof(ILambdaContext)) - { - args[i] = context; - } - else if (paramType == typeof(BedrockFunctionRequest)) - { - args[i] = input; - } - else if (IsBedrockParameter(paramType)) - { - var paramName = parameter.Name ?? $"arg{bedrockParamIndex}"; - - // AOT-compatible parameter access - direct type checks - // Array parameter handling - if (paramType.IsArray) - { - var jsonArrayStr = accessor.Get(paramName); - - if (!string.IsNullOrEmpty(jsonArrayStr)) - { - try - { - // AOT-compatible deserialization using source generation - if (paramType == typeof(string[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, - BedrockFunctionResolverContext.Default.StringArray); - else if (paramType == typeof(int[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, - BedrockFunctionResolverContext.Default.Int32Array); - else if (paramType == typeof(long[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, - BedrockFunctionResolverContext.Default.Int64Array); - else if (paramType == typeof(double[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, - BedrockFunctionResolverContext.Default.DoubleArray); - else if (paramType == typeof(bool[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, - BedrockFunctionResolverContext.Default.BooleanArray); - else if (paramType == typeof(decimal[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, - BedrockFunctionResolverContext.Default.DecimalArray); - else - args[i] = null; // Unsupported array type - } - catch (JsonException) - { - args[i] = null; - } - } - else - { - args[i] = null; - } - } - - if (paramType == typeof(string)) - args[i] = accessor.Get(paramName); - else if (paramType == typeof(int)) - args[i] = accessor.Get(paramName); - else if (paramType == typeof(long)) - args[i] = accessor.Get(paramName); - else if (paramType == typeof(double)) - args[i] = accessor.Get(paramName); - else if (paramType == typeof(bool)) - args[i] = accessor.Get(paramName); - else if (paramType == typeof(decimal)) - args[i] = accessor.Get(paramName); - else if (paramType == typeof(DateTime)) - args[i] = accessor.Get(paramName); - else if (paramType == typeof(Guid)) - args[i] = accessor.Get(paramName); - else if (paramType.IsEnum) - { - // For enums, get as string and parse - var strValue = accessor.Get(paramName); - args[i] = !string.IsNullOrEmpty(strValue) ? Enum.Parse(paramType, strValue) : null; - } - - bedrockParamIndex++; - } - else if (serviceProvider != null) - { - // Resolve from DI - args[i] = serviceProvider.GetService(paramType); - } - } + _handlers[name] = RegisterToolHandler(handler, name); + return this; + } + private Func RegisterToolHandler( + Delegate handler, string functionName) + { + return (input, context) => + { try { - // Execute the handler - var result = handler.DynamicInvoke(args); - - // Direct return for BedrockFunctionResponse - if (result is BedrockFunctionResponse output) - return output; - - // Handle async results with specific type checks (AOT-compatible) - if (result is Task outputTask) - return outputTask.Result; - if (result is Task stringTask) - return ConvertToOutput((TResult)(object)stringTask.Result, input); - if (result is Task intTask) - return ConvertToOutput((TResult)(object)intTask.Result, input); - if (result is Task boolTask) - return ConvertToOutput((TResult)(object)boolTask.Result, input); - if (result is Task doubleTask) - return ConvertToOutput((TResult)(object)doubleTask.Result, input); - if (result is Task longTask) - return ConvertToOutput((TResult)(object)longTask.Result, input); - if (result is Task decimalTask) - return ConvertToOutput((TResult)(object)decimalTask.Result, input); - if (result is Task dateTimeTask) - return ConvertToOutput((TResult)(object)dateTimeTask.Result, input); - if (result is Task guidTask) - return ConvertToOutput((TResult)(object)guidTask.Result, input); - if (result is Task objectTask) - return ConvertToOutput((TResult)objectTask.Result, input); - - // For regular Task with no result - if (result is not Task task) return ConvertToOutput(result, input); - task.GetAwaiter().GetResult(); - return BedrockFunctionResponse.WithText( - string.Empty, - input.ActionGroup, - name, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - + // Map parameters from Bedrock input and DI + var serviceProvider = (this as DiBedrockAgentFunctionResolver)?.ServiceProvider; + var args = _parameterMapper.MapParameters(handler.Method, input, context, serviceProvider); + + // Execute the handler and process result + return ExecuteHandlerAndProcessResult(handler, args, input, context, functionName); } catch (Exception ex) { @@ -519,15 +263,42 @@ public BedrockAgentFunctionResolver Tool( var innerException = ex.InnerException ?? ex; return BedrockFunctionResponse.WithText( $"Error when invoking tool: {innerException.Message}", - input.ActionGroup, - name, + input.ActionGroup, + functionName, input.SessionAttributes, input.PromptSessionAttributes, new Dictionary()); } }; + } - return this; + private BedrockFunctionResponse ExecuteHandlerAndProcessResult( + Delegate handler, + object?[] args, + BedrockFunctionRequest input, + ILambdaContext? context, + string functionName) + { + try + { + // Execute the handler + var result = handler.DynamicInvoke(args); + + // Process various result types + return _resultConverter.ProcessResult(result, input, functionName, context); + } + catch (Exception ex) + { + context?.Logger.LogError(ex.ToString()); + var innerException = ex.InnerException ?? ex; + return BedrockFunctionResponse.WithText( + $"Error when invoking tool: {innerException.Message}", + input.ActionGroup, + functionName, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } } /// @@ -536,19 +307,6 @@ public BedrockAgentFunctionResolver Tool( /// The Bedrock Agent input containing the function name and parameters /// Optional Lambda context /// The output from the function execution - /// - /// - /// // Lambda handler - /// public BedrockFunctionResponse FunctionHandler(BedrockFunctionRequest input, ILambdaContext context) - /// { - /// var resolver = new BedrockAgentFunctionResolver() - /// .Tool("GetWeather", (string city) => $"Weather in {city} is sunny") - /// .Tool("GetTime", () => DateTime.Now.ToString()); - /// - /// return resolver.Resolve(input, context); - /// } - /// - /// public BedrockFunctionResponse Resolve(BedrockFunctionRequest input, ILambdaContext? context = null) { return ResolveAsync(input, context).GetAwaiter().GetResult(); @@ -560,23 +318,6 @@ public BedrockFunctionResponse Resolve(BedrockFunctionRequest input, ILambdaCont /// The Bedrock Agent input containing the function name and parameters /// Optional Lambda context /// A task that completes with the output from the function execution - /// - /// - /// // Async Lambda handler - /// public async Task<BedrockFunctionResponse> FunctionHandler(BedrockFunctionRequest input, ILambdaContext context) - /// { - /// var resolver = new BedrockAgentFunctionResolver() - /// .Tool("GetWeatherAsync", async (string city) => { - /// // Simulate API call - /// await Task.Delay(100); - /// return $"Weather in {city} is sunny"; - /// }) - /// .Tool("GetTime", () => DateTime.Now.ToString()); - /// - /// return await resolver.ResolveAsync(input, context); - /// } - /// - /// public async Task ResolveAsync(BedrockFunctionRequest input, ILambdaContext? context = null) { @@ -624,115 +365,5 @@ private BedrockFunctionResponse HandleEvent(BedrockFunctionRequest input, ILambd input.PromptSessionAttributes, new Dictionary()); } - - private BedrockFunctionResponse ConvertToOutput(T result, BedrockFunctionRequest input) - { - string actionGroup = input.ActionGroup; - string function = input.Function; - - if (result == null) - { - return BedrockFunctionResponse.WithText( - string.Empty, - actionGroup, - function, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } - - // If result is already an BedrockFunctionResponse, ensure action group and function are set - if (result is BedrockFunctionResponse output) - { - // If the action group or function are not set in the output, use the provided values - if (string.IsNullOrEmpty(output.Response.ActionGroup)) - { - output.Response.ActionGroup = actionGroup; - } - - if (string.IsNullOrEmpty(output.Response.Function)) - { - output.Response.Function = function; - } - - return output; - } - - // For primitive types and strings, convert to string - if (result is string str) - { - return BedrockFunctionResponse.WithText( - str, - actionGroup, - function, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } - - if (result is int intVal) - { - return BedrockFunctionResponse.WithText( - intVal.ToString(CultureInfo.InvariantCulture), - actionGroup, - function, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } - - if (result is double doubleVal) - { - return BedrockFunctionResponse.WithText( - doubleVal.ToString(CultureInfo.InvariantCulture), - actionGroup, - function, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } - - if (result is bool boolVal) - { - return BedrockFunctionResponse.WithText( - boolVal.ToString(), - actionGroup, - function, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } - - if (result is long longVal) - { - return BedrockFunctionResponse.WithText( - longVal.ToString(CultureInfo.InvariantCulture), - actionGroup, - function, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } - - if (result is decimal decimalVal) - { - return BedrockFunctionResponse.WithText( - decimalVal.ToString(CultureInfo.InvariantCulture), - actionGroup, - function, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } - - // For any other type, use ToString() - return BedrockFunctionResponse.WithText( - result.ToString() ?? string.Empty, - actionGroup, - function, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } } } diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs new file mode 100644 index 00000000..85cb1a3e --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs @@ -0,0 +1,150 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Reflection; +using System.Text.Json; +using Amazon.Lambda.Core; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers +{ + /// + /// Maps parameters for Bedrock Agent function handlers + /// + public class ParameterMapper + { + private readonly ParameterTypeValidator _validator = new(); + + /// + /// Maps parameters for a handler method from a Bedrock function request + /// + /// The handler method + /// The Bedrock function request + /// The Lambda context + /// Optional service provider for dependency injection + /// Array of arguments to pass to the handler + public object?[] MapParameters( + MethodInfo methodInfo, + BedrockFunctionRequest input, + ILambdaContext? context, + IServiceProvider? serviceProvider) + { + var parameters = methodInfo.GetParameters(); + var args = new object?[parameters.Length]; + var accessor = new ParameterAccessor(input.Parameters); + var bedrockParamIndex = 0; + + for (var i = 0; i < parameters.Length; i++) + { + var parameter = parameters[i]; + var paramType = parameter.ParameterType; + + if (paramType == typeof(ILambdaContext)) + { + args[i] = context; + } + else if (paramType == typeof(BedrockFunctionRequest)) + { + args[i] = input; + } + else if (_validator.IsBedrockParameter(paramType)) + { + args[i] = MapBedrockParameter(paramType, parameter.Name ?? $"arg{bedrockParamIndex}", accessor); + bedrockParamIndex++; + } + else if (serviceProvider != null) + { + // Resolve from DI + args[i] = serviceProvider.GetService(paramType); + } + } + + return args; + } + + private object? MapBedrockParameter(Type paramType, string paramName, ParameterAccessor accessor) + { + // Array parameter handling + if (paramType.IsArray) + { + return MapArrayParameter(paramType, paramName, accessor); + } + + // Scalar parameter handling + return MapScalarParameter(paramType, paramName, accessor); + } + + private object? MapArrayParameter(Type paramType, string paramName, ParameterAccessor accessor) + { + var jsonArrayStr = accessor.Get(paramName); + + if (string.IsNullOrEmpty(jsonArrayStr)) + { + return null; + } + + try + { + // AOT-compatible deserialization using source generation + if (paramType == typeof(string[])) + return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.StringArray); + if (paramType == typeof(int[])) + return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.Int32Array); + if (paramType == typeof(long[])) + return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.Int64Array); + if (paramType == typeof(double[])) + return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.DoubleArray); + if (paramType == typeof(bool[])) + return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.BooleanArray); + if (paramType == typeof(decimal[])) + return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.DecimalArray); + } + catch (JsonException) + { + // Return null on error + } + + return null; + } + + private object? MapScalarParameter(Type paramType, string paramName, ParameterAccessor accessor) + { + if (paramType == typeof(string)) + return accessor.Get(paramName); + if (paramType == typeof(int)) + return accessor.Get(paramName); + if (paramType == typeof(long)) + return accessor.Get(paramName); + if (paramType == typeof(double)) + return accessor.Get(paramName); + if (paramType == typeof(bool)) + return accessor.Get(paramName); + if (paramType == typeof(decimal)) + return accessor.Get(paramName); + if (paramType == typeof(DateTime)) + return accessor.Get(paramName); + if (paramType == typeof(Guid)) + return accessor.Get(paramName); + if (paramType.IsEnum) + { + // For enums, get as string and parse + var strValue = accessor.Get(paramName); + return !string.IsNullOrEmpty(strValue) ? Enum.Parse(paramType, strValue) : null; + } + + return null; + } + } +} diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterTypeValidator.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterTypeValidator.cs new file mode 100644 index 00000000..19123d49 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterTypeValidator.cs @@ -0,0 +1,50 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers +{ + /// + /// Validates parameter types for Bedrock Agent functions + /// + public class ParameterTypeValidator + { + private static readonly HashSet BedrockParameterTypes = new() + { + typeof(string), + typeof(int), + typeof(long), + typeof(double), + typeof(bool), + typeof(decimal), + typeof(DateTime), + typeof(Guid), + typeof(string[]), + typeof(int[]), + typeof(long[]), + typeof(double[]), + typeof(bool[]), + typeof(decimal[]) + }; + + /// + /// Checks if a type is a valid Bedrock parameter type + /// + /// The type to check + /// True if the type is valid for Bedrock parameters + public bool IsBedrockParameter(Type type) => + BedrockParameterTypes.Contains(type) || type.IsEnum || + (type.IsArray && BedrockParameterTypes.Contains(type.GetElementType()!)); + } +} diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs new file mode 100644 index 00000000..65f5d9ed --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs @@ -0,0 +1,238 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Globalization; +using Amazon.Lambda.Core; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers +{ + /// + /// Converts handler results to BedrockFunctionResponse + /// + public class ResultConverter + { + /// + /// Processes results from handler functions and converts to BedrockFunctionResponse + /// + public BedrockFunctionResponse ProcessResult( + object? result, + BedrockFunctionRequest input, + string functionName, + ILambdaContext? context) + { + // Direct return for BedrockFunctionResponse + if (result is BedrockFunctionResponse output) + return EnsureResponseMetadata(output, input, functionName); + + // Handle async results with specific type checks (AOT-compatible) + if (result is Task outputTask) + return EnsureResponseMetadata(outputTask.Result, input, functionName); + + // Handle various Task types + if (result is Task task) + { + return HandleTaskResult(task, input, functionName); + } + + // Handle regular (non-task) results + return ConvertToOutput(result, input); + } + + private BedrockFunctionResponse HandleTaskResult(Task task, BedrockFunctionRequest input, string functionName) + { + // For Task + if (task is Task stringTask) + return ConvertToOutput((TResult)(object)stringTask.Result, input); + + // For Task + if (task is Task intTask) + return ConvertToOutput((TResult)(object)intTask.Result, input); + + // For Task + if (task is Task boolTask) + return ConvertToOutput((TResult)(object)boolTask.Result, input); + + // For Task + if (task is Task doubleTask) + return ConvertToOutput((TResult)(object)doubleTask.Result, input); + + // For Task + if (task is Task longTask) + return ConvertToOutput((TResult)(object)longTask.Result, input); + + // For Task + if (task is Task decimalTask) + return ConvertToOutput((TResult)(object)decimalTask.Result, input); + + // For Task + if (task is Task dateTimeTask) + return ConvertToOutput((TResult)(object)dateTimeTask.Result, input); + + // For Task + if (task is Task guidTask) + return ConvertToOutput((TResult)(object)guidTask.Result, input); + + // For Task + if (task is Task objectTask) + return ConvertToOutput((TResult)objectTask.Result, input); + + // For regular Task with no result + task.GetAwaiter().GetResult(); + return BedrockFunctionResponse.WithText( + string.Empty, + input.ActionGroup, + input.Function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + /// + /// Converts a result to a BedrockFunctionResponse + /// + public BedrockFunctionResponse ConvertToOutput(T result, BedrockFunctionRequest input) + { + string actionGroup = input.ActionGroup; + string function = input.Function; + + if (result == null) + { + return CreateEmptyResponse(input); + } + + // If result is already a BedrockFunctionResponse, ensure metadata is set + if (result is BedrockFunctionResponse output) + { + return EnsureResponseMetadata(output, input, function); + } + + // Handle primitive types + return ConvertPrimitiveToOutput(result, input); + } + + private BedrockFunctionResponse ConvertPrimitiveToOutput(T result, BedrockFunctionRequest input) + { + string actionGroup = input.ActionGroup; + string function = input.Function; + + // For primitive types and strings, convert to string + if (result is string str) + { + return BedrockFunctionResponse.WithText( + str, + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + if (result is int intVal) + { + return BedrockFunctionResponse.WithText( + intVal.ToString(CultureInfo.InvariantCulture), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + if (result is double doubleVal) + { + return BedrockFunctionResponse.WithText( + doubleVal.ToString(CultureInfo.InvariantCulture), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + if (result is bool boolVal) + { + return BedrockFunctionResponse.WithText( + boolVal.ToString(), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + if (result is long longVal) + { + return BedrockFunctionResponse.WithText( + longVal.ToString(CultureInfo.InvariantCulture), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + if (result is decimal decimalVal) + { + return BedrockFunctionResponse.WithText( + decimalVal.ToString(CultureInfo.InvariantCulture), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + // For any other type, use ToString() + return BedrockFunctionResponse.WithText( + result?.ToString() ?? string.Empty, + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + private BedrockFunctionResponse CreateEmptyResponse(BedrockFunctionRequest input) + { + return BedrockFunctionResponse.WithText( + string.Empty, + input.ActionGroup, + input.Function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + private BedrockFunctionResponse EnsureResponseMetadata( + BedrockFunctionResponse response, + BedrockFunctionRequest input, + string functionName) + { + // If the action group or function are not set in the output, use the provided values + if (string.IsNullOrEmpty(response.Response.ActionGroup)) + { + response.Response.ActionGroup = input.ActionGroup; + } + + if (string.IsNullOrEmpty(response.Response.Function)) + { + response.Response.Function = functionName; + } + + return response; + } + } +} From 30de3f31d68c388ad452abb71798f878c7443ce3 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 May 2025 09:39:17 +0100 Subject: [PATCH 14/52] refactor sonar warnings --- .../Helpers/ResultConverter.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs index 65f5d9ed..ee668b28 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs @@ -44,14 +44,14 @@ public BedrockFunctionResponse ProcessResult( // Handle various Task types if (result is Task task) { - return HandleTaskResult(task, input, functionName); + return HandleTaskResult(task, input); } // Handle regular (non-task) results return ConvertToOutput(result, input); } - private BedrockFunctionResponse HandleTaskResult(Task task, BedrockFunctionRequest input, string functionName) + private BedrockFunctionResponse HandleTaskResult(Task task, BedrockFunctionRequest input) { // For Task if (task is Task stringTask) @@ -105,10 +105,9 @@ private BedrockFunctionResponse HandleTaskResult(Task task, BedrockFunc /// public BedrockFunctionResponse ConvertToOutput(T result, BedrockFunctionRequest input) { - string actionGroup = input.ActionGroup; - string function = input.Function; + var function = input.Function; - if (result == null) + if (EqualityComparer.Default.Equals(result, default(T))) { return CreateEmptyResponse(input); } @@ -125,8 +124,8 @@ public BedrockFunctionResponse ConvertToOutput(T result, BedrockFunctionReque private BedrockFunctionResponse ConvertPrimitiveToOutput(T result, BedrockFunctionRequest input) { - string actionGroup = input.ActionGroup; - string function = input.Function; + var actionGroup = input.ActionGroup; + var function = input.Function; // For primitive types and strings, convert to string if (result is string str) From 5dabe414bfdd01765f005a7f158acd8b12247596 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 May 2025 10:08:37 +0100 Subject: [PATCH 15/52] add unit tests for ParameterAccessor, ParameterMapper, ParameterTypeValidator, ResultConverter, and BedrockAgentFunctionResolver --- ...ambda.Powertools.EventHandler.Tests.csproj | 6 +- ...ockAgentFunctionResolverAdditionalTests.cs | 212 ++++++++++++ .../BedrockAgentFunctionResolverTests.cs | 5 +- .../Helpers/ParameterAccessorTests.cs | 197 +++++++++++ .../Helpers/ParameterMapperTests.cs | 311 ++++++++++++++++++ .../Helpers/ParameterTypeValidatorTests.cs | 49 +++ .../Helpers/ResultConverterTests.cs | 276 ++++++++++++++++ .../bedrockFunctionEvent.json | 0 .../{ => EventHandler}/AppSyncEventsTests.cs | 2 +- .../RouteHandlerRegistryTests.cs | 2 +- .../bedrockFunctionEvent2.json | 27 -- 11 files changed, 1052 insertions(+), 35 deletions(-) create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverAdditionalTests.cs rename libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/{ => BedrockAgentFunction}/BedrockAgentFunctionResolverTests.cs (99%) create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterAccessorTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterMapperTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterTypeValidatorTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ResultConverterTests.cs rename libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/{ => BedrockAgentFunction}/bedrockFunctionEvent.json (100%) rename libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/{ => EventHandler}/AppSyncEventsTests.cs (99%) rename libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/{ => EventHandler}/RouteHandlerRegistryTests.cs (99%) delete mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj index 1d0b8362..2d37bab6 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj @@ -17,6 +17,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -42,11 +43,8 @@ PreserveNewest - - PreserveNewest - - + PreserveNewest diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverAdditionalTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverAdditionalTests.cs new file mode 100644 index 00000000..62d09fde --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverAdditionalTests.cs @@ -0,0 +1,212 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.EventHandler.Resolvers; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +namespace AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction +{ + public class BedrockAgentFunctionResolverAdditionalTests + { + [Fact] + public async Task ResolveAsync_WithValidInput_ReturnsResult() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("AsyncTest", () => "Async result"); + + var input = new BedrockFunctionRequest { Function = "AsyncTest" }; + var context = new TestLambdaContext(); + + // Act + var result = await resolver.ResolveAsync(input, context); + + // Assert + Assert.Equal("Async result", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void Tool_WithNullHandler_ThrowsException() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + Func nullHandler = null!; + + // Act/Assert + Assert.Throws(() => resolver.Tool("NullTest", nullHandler)); + } + + [Fact] + public void Resolve_WithNullFunction_ReturnsErrorResponse() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + var input = new BedrockFunctionRequest { Function = null }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("No tool specified in the request", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void Resolve_WithEmptyFunction_ReturnsErrorResponse() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + var input = new BedrockFunctionRequest { Function = "" }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("No tool specified in the request", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void Tool_WithHandlerThrowingException_ReturnsErrorResponse() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("ExceptionTest", (BedrockFunctionRequest input, ILambdaContext ctx) => { + throw new InvalidOperationException("Handler exception"); + return new BedrockFunctionResponse(); + }); + + var input = new BedrockFunctionRequest { Function = "ExceptionTest" }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Error when invoking tool: Handler exception", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void Tool_WithDynamicInvokeException_ReturnsErrorResponse() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("ExceptionTest", (Func)(() => { + throw new InvalidOperationException("Dynamic invoke exception"); + })); + + var input = new BedrockFunctionRequest { Function = "ExceptionTest" }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains("Error when invoking tool", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void Tool_ObjectFunctionRegistration_ReturnsObjectAsString() + { + // Arrange + var testObject = new TestObject { Id = 123, Name = "Test" }; + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("ObjectTest", () => testObject); + + var input = new BedrockFunctionRequest { Function = "ObjectTest" }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal(testObject.ToString(), result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public async Task Resolve_WithAsyncTask_HandlesCorrectly() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("AsyncTaskTest", async (string message) => { + await Task.Delay(10); // Simulate async work + return $"Processed: {message}"; + }); + + var input = new BedrockFunctionRequest { + Function = "AsyncTaskTest", + Parameters = new List { + new Parameter { Name = "message", Value = "hello", Type = "String" } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Processed: hello", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void Tool_WithBedrockFunctionResponseHandlerNoContext_MapsCorrectly() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("NoContextTest", (BedrockFunctionRequest request) => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "NoContextTest", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "No context needed" } + } + } + } + }); + + var input = new BedrockFunctionRequest { Function = "NoContextTest" }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("No context needed", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void Tool_WithBedrockFunctionResponseHandler_MapsCorrectly() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("ResponseTest", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "ResponseTest", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Direct response" } + } + } + } + }); + + var input = new BedrockFunctionRequest { Function = "ResponseTest" }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Direct response", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + private class TestObject + { + public int Id { get; set; } + public string Name { get; set; } = ""; + + public override string ToString() => $"{Name} (ID: {Id})"; + } + } +} diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverTests.cs similarity index 99% rename from libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs rename to libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverTests.cs index 50b28fe6..5e630214 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverTests.cs @@ -2,13 +2,14 @@ using System.Text; using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.EventHandler.Resolvers; using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; using Microsoft.Extensions.DependencyInjection; #pragma warning disable CS0162 // Unreachable code detected -// ReSharper disable once CheckNamespace -namespace AWS.Lambda.Powertools.EventHandler.Resolvers.Tests; + +namespace AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction; public class BedrockAgentFunctionResolverTests { diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterAccessorTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterAccessorTests.cs new file mode 100644 index 00000000..34a8f246 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterAccessorTests.cs @@ -0,0 +1,197 @@ +using AWS.Lambda.Powertools.EventHandler.Resolvers; + +namespace AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction.Helpers +{ + public class ParameterAccessorTests + { + [Fact] + public void Get_WithStringParameter_ReturnsValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "name", Value = "TestValue", Type = "String" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.Get("name"); + + // Assert + Assert.Equal("TestValue", result); + } + + [Fact] + public void Get_WithIntParameter_ReturnsValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "age", Value = "30", Type = "Number" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.Get("age"); + + // Assert + Assert.Equal(30, result); + } + + [Fact] + public void Get_WithBoolParameter_ReturnsValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "active", Value = "true", Type = "Boolean" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.Get("active"); + + // Assert + Assert.True(result); + } + + [Fact] + public void Get_WithLongParameter_ReturnsValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "bigNumber", Value = "9223372036854775807", Type = "Number" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.Get("bigNumber"); + + // Assert + Assert.Equal(9223372036854775807, result); + } + + [Fact] + public void Get_WithDoubleParameter_ReturnsValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "price", Value = "99.99", Type = "Number" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.Get("price"); + + // Assert + Assert.Equal(99.99, result); + } + + [Fact] + public void Get_WithDecimalParameter_ReturnsValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "amount", Value = "123.456", Type = "Number" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.Get("amount"); + + // Assert + Assert.Equal(123.456m, result); + } + + [Fact] + public void Get_WithNonExistentParameter_ReturnsDefault() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "existing", Value = "value", Type = "String" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var stringResult = accessor.Get("nonExistent"); + var intResult = accessor.Get("nonExistent"); + var boolResult = accessor.Get("nonExistent"); + + // Assert + Assert.Null(stringResult); + Assert.Equal(0, intResult); + Assert.False(boolResult); + } + + [Fact] + public void Get_WithCaseSensitivity_WorksCaseInsensitively() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "userName", Value = "John", Type = "String" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result1 = accessor.Get("userName"); + var result2 = accessor.Get("UserName"); + var result3 = accessor.Get("USERNAME"); + + // Assert + Assert.Equal("John", result1); + Assert.Equal("John", result2); + Assert.Equal("John", result3); + } + + [Fact] + public void Get_WithNullParameters_ReturnsDefault() + { + // Arrange + var accessor = new ParameterAccessor(null); + + // Act + var stringResult = accessor.Get("any"); + var intResult = accessor.Get("any"); + + // Assert + Assert.Null(stringResult); + Assert.Equal(0, intResult); + } + + [Fact] + public void Get_WithInvalidType_ReturnsDefault() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "number", Value = "not-a-number", Type = "Number" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.Get("number"); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void Get_WithEmptyParameters_ReturnsDefault() + { + // Arrange + var parameters = new List(); + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.Get("anything"); + + // Assert + Assert.Null(result); + } + } +} diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterMapperTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterMapperTests.cs new file mode 100644 index 00000000..b4cd5705 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterMapperTests.cs @@ -0,0 +1,311 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.EventHandler.Resolvers; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; +using NSubstitute; + +namespace AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction.Helpers +{ + public class ParameterMapperTests + { + private readonly ParameterMapper _mapper = new(); + + [Fact] + public void MapParameters_WithNoParameters_ReturnsEmptyArray() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.NoParameters))!; + var input = new BedrockFunctionRequest(); + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Empty(result); + } + + [Fact] + public void MapParameters_WithLambdaContext_MapsCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithLambdaContext))!; + var input = new BedrockFunctionRequest(); + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + Assert.Same(context, result[0]); + } + + [Fact] + public void MapParameters_WithBedrockFunctionRequest_MapsCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithBedrockFunctionRequest))!; + var input = new BedrockFunctionRequest(); + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + Assert.Same(input, result[0]); + } + + [Fact] + public void MapParameters_WithStringParameter_MapsCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithStringParameter))!; + var input = new BedrockFunctionRequest + { + Parameters = new List + { + new() { Name = "name", Value = "TestValue", Type = "String" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + Assert.Equal("TestValue", result[0]); + } + + [Fact] + public void MapParameters_WithIntParameter_MapsCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithIntParameter))!; + var input = new BedrockFunctionRequest + { + Parameters = new List + { + new() { Name = "value", Value = "42", Type = "Number" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + Assert.Equal(42, result[0]); + } + + [Fact] + public void MapParameters_WithBoolParameter_MapsCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithBoolParameter))!; + var input = new BedrockFunctionRequest + { + Parameters = new List + { + new() { Name = "flag", Value = "true", Type = "Boolean" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + Assert.True((bool)result[0]!); + } + + [Fact] + public void MapParameters_WithEnumParameter_MapsCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithEnumParameter))!; + var input = new BedrockFunctionRequest + { + Parameters = new List + { + new() { Name = "testEnum", Value = "Option2", Type = "String" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + Assert.Equal(TestEnum.Option2, result[0]); + } + + [Fact] + public void MapParameters_WithStringArrayParameter_MapsCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithStringArrayParameter))!; + var input = new BedrockFunctionRequest + { + Parameters = new List + { + new() { Name = "values", Value = "[\"one\",\"two\",\"three\"]", Type = "String" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + var array = (string[])result[0]!; + Assert.Equal(3, array.Length); + Assert.Equal("one", array[0]); + Assert.Equal("two", array[1]); + Assert.Equal("three", array[2]); + } + + [Fact] + public void MapParameters_WithIntArrayParameter_MapsCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithIntArrayParameter))!; + var input = new BedrockFunctionRequest + { + Parameters = new List + { + new() { Name = "values", Value = "[1,2,3]", Type = "String" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + var array = (int[])result[0]!; + Assert.Equal(3, array.Length); + Assert.Equal(1, array[0]); + Assert.Equal(2, array[1]); + Assert.Equal(3, array[2]); + } + + [Fact] + public void MapParameters_WithInvalidJsonArray_ReturnsNull() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithStringArrayParameter))!; + var input = new BedrockFunctionRequest + { + Parameters = new List + { + new() { Name = "values", Value = "[invalid json]", Type = "String" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + Assert.Null(result[0]); + } + + [Fact] + public void MapParameters_WithServiceProvider_ResolvesService() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithDependencyInjection))!; + var input = new BedrockFunctionRequest(); + var context = new TestLambdaContext(); + + // Create a test service + var testService = new TestService(); + + // Setup service provider + var serviceProvider = Substitute.For(); + serviceProvider.GetService(typeof(ITestService)).Returns(testService); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, serviceProvider); + + // Assert + Assert.Equal(3, result.Length); + Assert.Same(context, result[0]); + Assert.Same(input, result[1]); + Assert.Same(testService, result[2]); + } + + [Fact] + public void MapParameters_WithMultipleParameterTypes_MapsAllCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithMultipleParameterTypes))!; + var input = new BedrockFunctionRequest + { + Parameters = new List + { + new() { Name = "name", Value = "TestUser", Type = "String" }, + new() { Name = "age", Value = "30", Type = "Number" }, + new() { Name = "isActive", Value = "true", Type = "Boolean" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Equal(4, result.Length); + Assert.Equal("TestUser", result[0]); + Assert.Equal(30, result[1]); + Assert.True((bool)result[2]!); + Assert.Same(context, result[3]); + } + + public class TestMethodsClass + { + public void NoParameters() { } + + public void WithLambdaContext(ILambdaContext context) { } + + public void WithBedrockFunctionRequest(BedrockFunctionRequest request) { } + + public void WithStringParameter(string name) { } + + public void WithIntParameter(int value) { } + + public void WithBoolParameter(bool flag) { } + + public void WithEnumParameter(TestEnum testEnum) { } + + public void WithStringArrayParameter(string[] values) { } + + public void WithIntArrayParameter(int[] values) { } + + public void WithDependencyInjection(ILambdaContext context, BedrockFunctionRequest request, ITestService service) { } + + public void WithMultipleParameterTypes(string name, int age, bool isActive, ILambdaContext context) { } + } + + public interface ITestService { } + + public class TestService : ITestService { } + + public enum TestEnum + { + Option1, + Option2, + Option3 + } + } +} diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterTypeValidatorTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterTypeValidatorTests.cs new file mode 100644 index 00000000..b8ec3353 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterTypeValidatorTests.cs @@ -0,0 +1,49 @@ +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers; + +namespace AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction.Helpers +{ + public class ParameterTypeValidatorTests + { + private readonly ParameterTypeValidator _validator = new(); + + [Theory] + [InlineData(typeof(string), true)] + [InlineData(typeof(int), true)] + [InlineData(typeof(long), true)] + [InlineData(typeof(double), true)] + [InlineData(typeof(bool), true)] + [InlineData(typeof(decimal), true)] + [InlineData(typeof(DateTime), true)] + [InlineData(typeof(Guid), true)] + [InlineData(typeof(string[]), true)] + [InlineData(typeof(int[]), true)] + [InlineData(typeof(long[]), true)] + [InlineData(typeof(double[]), true)] + [InlineData(typeof(bool[]), true)] + [InlineData(typeof(decimal[]), true)] + [InlineData(typeof(TestEnum), true)] // Enum should be valid + [InlineData(typeof(object), false)] + [InlineData(typeof(Dictionary), false)] + [InlineData(typeof(List), false)] + [InlineData(typeof(float), false)] + [InlineData(typeof(char), false)] + [InlineData(typeof(byte), false)] + [InlineData(typeof(float[]), false)] + [InlineData(typeof(object[]), false)] + public void IsBedrockParameter_WithVariousTypes_ReturnsExpectedResult(Type type, bool expected) + { + // Act + var result = _validator.IsBedrockParameter(type); + + // Assert + Assert.Equal(expected, result); + } + + private enum TestEnum + { + One, + Two, + Three + } + } +} diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ResultConverterTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ResultConverterTests.cs new file mode 100644 index 00000000..437118e5 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ResultConverterTests.cs @@ -0,0 +1,276 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +namespace AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction.Helpers +{ + public class ResultConverterTests + { + private readonly ResultConverter _converter = new(); + private readonly BedrockFunctionRequest _defaultInput = new() + { + Function = "TestFunction", + ActionGroup = "TestGroup", + SessionAttributes = new Dictionary { { "testKey", "testValue" } }, + PromptSessionAttributes = new Dictionary { { "promptKey", "promptValue" } } + }; + private readonly string _functionName = "TestFunction"; + private readonly ILambdaContext _context = new TestLambdaContext(); + + [Fact] + public void ProcessResult_WithBedrockFunctionResponse_ReturnsUnchanged() + { + // Arrange + var response = BedrockFunctionResponse.WithText( + "Test response", + "TestGroup", + "TestFunction", + new Dictionary(), + new Dictionary(), + new Dictionary()); + + // Act + var result = _converter.ProcessResult(response, _defaultInput, _functionName, _context); + + // Assert + Assert.Same(response, result); + } + + [Fact] + public void ProcessResult_WithNullValue_ReturnsEmptyResponse() + { + // Arrange + object? nullValue = null; + + // Act + var result = _converter.ProcessResult(nullValue, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal(string.Empty, result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal(_defaultInput.ActionGroup, result.Response.ActionGroup); + Assert.Equal(_defaultInput.Function, result.Response.Function); + } + + [Fact] + public void ProcessResult_WithStringValue_ReturnsTextResponse() + { + // Arrange + var stringValue = "Hello, world!"; + + // Act + var result = _converter.ProcessResult(stringValue, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal(stringValue, result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void ProcessResult_WithIntValue_ReturnsTextResponse() + { + // Arrange + var intValue = 42; + + // Act + var result = _converter.ProcessResult(intValue, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("42", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void ProcessResult_WithDecimalValue_ReturnsTextResponse() + { + // Arrange + var decimalValue = 42.5m; + + // Act + var result = _converter.ProcessResult(decimalValue, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("42.5", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void ProcessResult_WithBoolValue_ReturnsTextResponse() + { + // Arrange + var boolValue = true; + + // Act + var result = _converter.ProcessResult(boolValue, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("True", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void ProcessResult_WithObjectValue_ReturnsToString() + { + // Arrange + var testObject = new TestObject { Name = "Test", Value = 42 }; + + // Act + var result = _converter.ProcessResult(testObject, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal(testObject.ToString(), result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public async Task ProcessResult_WithTaskStringResult_ReturnsTextResponse() + { + // Arrange + Task task = Task.FromResult("Async result"); + + // Act + var result = _converter.ProcessResult(task, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("Async result", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public async Task ProcessResult_WithTaskIntResult_ReturnsTextResponse() + { + // Arrange + Task task = Task.FromResult(42); + + // Act + var result = _converter.ProcessResult(task, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("42", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public async Task ProcessResult_WithTaskBoolResult_ReturnsTextResponse() + { + // Arrange + Task task = Task.FromResult(true); + + // Act + var result = _converter.ProcessResult(task, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("True", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public async Task ProcessResult_WithVoidTask_ReturnsEmptyResponse() + { + // Arrange + Task task = Task.CompletedTask; + + // Act + var result = _converter.ProcessResult(task, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal(string.Empty, result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public async Task ProcessResult_WithTaskBedrockResponse_ReturnsResponse() + { + // Arrange + var response = BedrockFunctionResponse.WithText( + "Async response", + "AsyncGroup", + "AsyncFunction", + new Dictionary(), + new Dictionary(), + new Dictionary()); + + Task task = Task.FromResult(response); + + // Act + var result = _converter.ProcessResult(task, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("Async response", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("AsyncGroup", result.Response.ActionGroup); + Assert.Equal("AsyncFunction", result.Response.Function); + } + + [Fact] + public void EnsureResponseMetadata_WithEmptyMetadata_FillsFromInput() + { + // Arrange + var response = BedrockFunctionResponse.WithText( + "Test response", + "", // Empty action group + "", // Empty function name + _defaultInput.SessionAttributes, + _defaultInput.PromptSessionAttributes, + new Dictionary()); + + // Act + var result = _converter.ConvertToOutput(response, _defaultInput); + + // Assert + Assert.Equal("Test response", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal(_defaultInput.ActionGroup, result.Response.ActionGroup); // Filled from input + Assert.Equal(_defaultInput.Function, result.Response.Function); // Filled from input + } + + [Fact] + public void ConvertToOutput_PreservesSessionAttributes() + { + // Arrange + var sessionAttributes = new Dictionary { { "userID", "test123" } }; + var promptAttributes = new Dictionary { { "context", "testing" } }; + + var input = new BedrockFunctionRequest + { + Function = "TestFunction", + ActionGroup = "TestGroup", + SessionAttributes = sessionAttributes, + PromptSessionAttributes = promptAttributes + }; + + // Act + var result = _converter.ConvertToOutput("Test response", input); + + // Assert + Assert.Equal(sessionAttributes, result.SessionAttributes); + Assert.Equal(promptAttributes, result.PromptSessionAttributes); + } + + [Fact] + public void ProcessResult_WithLongValue_ReturnsTextResponse() + { + // Arrange + long longValue = 9223372036854775807; + + // Act + var result = _converter.ProcessResult(longValue, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("9223372036854775807", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void ProcessResult_WithDoubleValue_ReturnsTextResponse() + { + // Arrange + double doubleValue = 123.456; + + // Act + var result = _converter.ProcessResult(doubleValue, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("123.456", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + private class TestObject + { + public string Name { get; set; } = ""; + public int Value { get; set; } + + public override string ToString() + { + return $"{Name}:{Value}"; + } + } + } +} diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent.json b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/bedrockFunctionEvent.json similarity index 100% rename from libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent.json rename to libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/bedrockFunctionEvent.json diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AppSyncEventsTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/EventHandler/AppSyncEventsTests.cs similarity index 99% rename from libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AppSyncEventsTests.cs rename to libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/EventHandler/AppSyncEventsTests.cs index 0b8103f4..07c0e9fa 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AppSyncEventsTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/EventHandler/AppSyncEventsTests.cs @@ -6,7 +6,7 @@ #pragma warning disable CS8604 // Possible null reference argument. #pragma warning disable CS8602 // Dereference of a possibly null reference. -namespace AWS.Lambda.Powertools.EventHandler.Tests; +namespace AWS.Lambda.Powertools.EventHandler; public class AppSyncEventsTests { diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/RouteHandlerRegistryTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/EventHandler/RouteHandlerRegistryTests.cs similarity index 99% rename from libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/RouteHandlerRegistryTests.cs rename to libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/EventHandler/RouteHandlerRegistryTests.cs index f0437dc9..ac712da6 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/RouteHandlerRegistryTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/EventHandler/RouteHandlerRegistryTests.cs @@ -5,7 +5,7 @@ #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. #pragma warning disable CS8602 // Dereference of a possibly null reference. -namespace AWS.Lambda.Powertools.EventHandler.Tests; +namespace AWS.Lambda.Powertools.EventHandler; [SuppressMessage("Usage", "xUnit1031:Do not use blocking task operations in test method")] public class RouteHandlerRegistryTests diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json deleted file mode 100644 index 18e21694..00000000 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "messageVersion": "1.0", - "function": "get_weather_city", - "parameters": [ - { - "name": "month", - "type": "number", - "value": "5" - }, - { - "name": "city", - "type": "string", - "value": "Lisbon" - } - ], - "sessionId": "533568316194812", - "agent": { - "name": "powertools-function-agent", - "version": "DRAFT", - "id": "AVMWXZYN4X", - "alias": "TSTALIASID" - }, - "actionGroup": "action_group_quick_start_hgo6p", - "sessionAttributes": {}, - "promptSessionAttributes": {}, - "inputText": "weather in london?" -} \ No newline at end of file From 5bcba07e9051576d25049c39c90892e4fb82e889 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 May 2025 10:38:32 +0100 Subject: [PATCH 16/52] refactor: change access modifiers to internal for ParameterAccessor, ParameterMapper, ParameterTypeValidator, and ResultConverter; enhance error handling in ParameterAccessor --- README.md | 12 ++ .../{ => Helpers}/ParameterAccessor.cs | 23 ++- .../Helpers/ParameterMapper.cs | 2 +- .../Helpers/ParameterTypeValidator.cs | 2 +- .../Helpers/ResultConverter.cs | 2 +- ...rockAgentFunctionResolverExceptionTests.cs | 68 +++++++++ .../Helpers/ParameterAccessorTests.cs | 141 ++++++++++++++++++ 7 files changed, 242 insertions(+), 8 deletions(-) rename libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/{ => Helpers}/ParameterAccessor.cs (86%) create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverExceptionTests.cs diff --git a/README.md b/README.md index d3ac8714..bb75d283 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,10 @@ Powertools for AWS Lambda (.NET) provides three core utilities: * **[Batch Processing](https://docs.powertools.aws.dev/lambda/dotnet/utilities/batch-processing/)** - The batch processing utility handles partial failures when processing batches from Amazon SQS, Amazon Kinesis Data Streams, and Amazon DynamoDB Streams. +* **[Event Handler AppSync Events](https://docs.powertools.aws.dev/lambda/dotnet/core/event_handler/appsync_events/)** - The event handler AppSync Events utility provides a simple way to handle AppSync events in your Lambda functions. It allows you to easily parse the event and access the data you need, without having to write complex code. + +* **[Event Handler Bedrock Agent Functions](https://docs.powertools.aws.dev/lambda/dotnet/core/event_handler/bedrock_agent_function/)** - The event handler Bedrock Agent Functions utility provides a simple way to handle Amazon Bedrock agent function events in your Lambda functions. It allows you to easily parse the event and access the data you need, without having to write complex code. + ### Installation The Powertools for AWS Lambda (.NET) utilities (.NET 6 and .NET 8) are available as NuGet packages. You can install the packages from [NuGet Gallery](https://www.nuget.org/packages?q=AWS+Lambda+Powertools*) or from Visual Studio editor by searching `AWS.Lambda.Powertools*` to see various utilities available. @@ -63,6 +67,14 @@ The Powertools for AWS Lambda (.NET) utilities (.NET 6 and .NET 8) are available `dotnet add package AWS.Lambda.Powertools.BatchProcessing` +* [AWS.Lambda.Powertools.EventHandler.AppSyncEvents](https://www.nuget.org/packages/AWS.Lambda.Powertools.EventHandler): + + `dotnet add package AWS.Lambda.Powertools.EventHandler` + +* [AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction](https://www.nuget.org/packages/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction): + + `dotnet add package AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction` + ## Examples We have provided examples focused specifically on each of the utilities. Each solution comes with an AWS Serverless Application Model (AWS SAM) templates to run your functions as a Zip package using the AWS Lambda .NET 6 or .NET 8 managed runtime; or as a container package using the AWS base images for .NET. diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterAccessor.cs similarity index 86% rename from libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs rename to libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterAccessor.cs index e81675ca..277905ea 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterAccessor.cs @@ -15,13 +15,12 @@ using System.Globalization; -// ReSharper disable once CheckNamespace -namespace AWS.Lambda.Powertools.EventHandler.Resolvers; +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers; /// /// Provides strongly-typed access to the parameters of an agent function call. /// -public class ParameterAccessor +internal class ParameterAccessor { private readonly List _parameters; @@ -74,7 +73,21 @@ public T GetOrDefault(string name, T defaultValue) return defaultValue; } - return ConvertParameter(parameter); + try + { + var result = ConvertParameter(parameter); + // If conversion returns default value but we have a non-null parameter, + // that means conversion failed, so return the provided default value + if (EqualityComparer.Default.Equals(result, default) && parameter.Value != null) + { + return defaultValue; + } + return result; + } + catch + { + return defaultValue; + } } private static T ConvertParameter(Parameter? parameter) @@ -138,4 +151,4 @@ private static T ConvertParameter(Parameter? parameter) // Return default for array and complex types return default!; } -} \ No newline at end of file +} diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs index 85cb1a3e..1c9edf77 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs @@ -23,7 +23,7 @@ namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Help /// /// Maps parameters for Bedrock Agent function handlers /// - public class ParameterMapper + internal class ParameterMapper { private readonly ParameterTypeValidator _validator = new(); diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterTypeValidator.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterTypeValidator.cs index 19123d49..164c4241 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterTypeValidator.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterTypeValidator.cs @@ -18,7 +18,7 @@ namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Help /// /// Validates parameter types for Bedrock Agent functions /// - public class ParameterTypeValidator + internal class ParameterTypeValidator { private static readonly HashSet BedrockParameterTypes = new() { diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs index ee668b28..72bbe383 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs @@ -22,7 +22,7 @@ namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Help /// /// Converts handler results to BedrockFunctionResponse /// - public class ResultConverter + internal class ResultConverter { /// /// Processes results from handler functions and converts to BedrockFunctionResponse diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverExceptionTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverExceptionTests.cs new file mode 100644 index 00000000..b05c3f42 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverExceptionTests.cs @@ -0,0 +1,68 @@ +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.EventHandler.Resolvers; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +namespace AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction +{ + public class BedrockAgentFunctionResolverExceptionTests + { + [Fact] + public void RegisterToolHandler_WithParameterMappingException_ReturnsErrorResponse() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + + // Register a tool that requires a complex parameter that can't be mapped automatically + resolver.Tool("ComplexTest", (TestComplexType complex) => $"Name: {complex.Name}"); + + var input = new BedrockFunctionRequest + { + Function = "ComplexTest", + Parameters = new List + { + // This parameter can't be automatically mapped to the complex type + new Parameter { Name = "complex", Value = "{\"name\":\"Test\"}", Type = "String" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + // This should trigger the parameter mapping exception path + Assert.Contains("Error when invoking tool:", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void RegisterToolHandler_WithNestedExceptionInDelegateInvoke_HandlesCorrectly() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + + // Register a tool with a delegate that will throw an exception with inner exception + resolver.Tool("NestedExceptionTest", () => { + throw new AggregateException("Outer exception", + new ApplicationException("Inner exception message")); + return "Should not reach here"; + }); + + var input = new BedrockFunctionRequest { Function = "NestedExceptionTest" }; + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + // The error should contain the inner exception message + Assert.Contains("Inner exception message", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + // A test complex type that can't be automatically mapped from parameters + private class TestComplexType + { + public string Name { get; set; } = ""; + public int Value { get; set; } + } + } +} diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterAccessorTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterAccessorTests.cs index 34a8f246..b5d54046 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterAccessorTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterAccessorTests.cs @@ -1,4 +1,5 @@ using AWS.Lambda.Powertools.EventHandler.Resolvers; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers; namespace AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction.Helpers { @@ -193,5 +194,145 @@ public void Get_WithEmptyParameters_ReturnsDefault() // Assert Assert.Null(result); } + + [Fact] + public void GetAt_WithValidIndex_ReturnsValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "first", Value = "Value1", Type = "String" }, + new Parameter { Name = "second", Value = "42", Type = "Number" }, + new Parameter { Name = "third", Value = "true", Type = "Boolean" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var stringResult = accessor.GetAt(0); + var intResult = accessor.GetAt(1); + var boolResult = accessor.GetAt(2); + + // Assert + Assert.Equal("Value1", stringResult); + Assert.Equal(42, intResult); + Assert.True(boolResult); + } + + [Fact] + public void GetAt_WithInvalidIndex_ReturnsDefaultValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "param", Value = "Value", Type = "String" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var negativeIndexResult = accessor.GetAt(-1); + var tooLargeIndexResult = accessor.GetAt(1); + + // Assert + Assert.Null(negativeIndexResult); + Assert.Null(tooLargeIndexResult); + } + + [Fact] + public void GetAt_WithNullParameters_ReturnsDefaultValue() + { + // Arrange + var accessor = new ParameterAccessor(null); + + // Act + var result = accessor.GetAt(0); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetAt_WithNullValue_ReturnsDefaultValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "param", Value = null, Type = "String" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.GetAt(0); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetOrDefault_WithExistingParameter_ReturnsValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "name", Value = "TestValue", Type = "String" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.GetOrDefault("name", "DefaultValue"); + + // Assert + Assert.Equal("TestValue", result); + } + + [Fact] + public void GetOrDefault_WithNonExistentParameter_ReturnsDefaultValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "existing", Value = "value", Type = "String" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.GetOrDefault("nonExistent", "DefaultValue"); + + // Assert + Assert.Equal("DefaultValue", result); + } + + [Fact] + public void GetOrDefault_WithNullValue_ReturnsDefaultValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "param", Value = null, Type = "String" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.GetOrDefault("param", "DefaultValue"); + + // Assert + Assert.Equal("DefaultValue", result); + } + + [Fact] + public void GetOrDefault_WithInvalidConversion_ReturnsDefaultValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "invalidNumber", Value = "not-a-number", Type = "Number" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.GetOrDefault("invalidNumber", 999); + + // Assert + Assert.Equal(999, result); + } } } From 3756ef3327f8175f282ef0a915d63475a6296706 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 May 2025 10:47:43 +0100 Subject: [PATCH 17/52] Update README.md Signed-off-by: Henrique Graca <999396+hjgraca@users.noreply.github.com> --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d3ac8714..5946eb4c 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ The following companies, among others, use Powertools: * [Caylent](https://caylent.com/) * [Pushpay](https://pushpay.com/) +* [Instil Software](https://instil.co/) ### Sharing your work From 96f205c2c441cba07bec6709e63e0091c5e9b705 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 May 2025 10:50:36 +0100 Subject: [PATCH 18/52] Update index.md Signed-off-by: Henrique Graca <999396+hjgraca@users.noreply.github.com> --- docs/index.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/index.md b/docs/index.md index 29875d66..08a46cac 100644 --- a/docs/index.md +++ b/docs/index.md @@ -166,6 +166,9 @@ Knowing which companies are using this library is important to help prioritize t [**Pushpay**](https://pushpay.com/){target="_blank" rel="nofollow"} { .card } +[**Instil Software**](https://instil.co/){target="_blank" rel="nofollow"} +{ .card } + ## Tenets From e75a9d0901b808d0015c79741e44cbf05a21c721 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 May 2025 11:31:35 +0100 Subject: [PATCH 19/52] Update version.json Signed-off-by: Henrique Graca <999396+hjgraca@users.noreply.github.com> --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index cb5c98bc..53805977 100644 --- a/version.json +++ b/version.json @@ -10,6 +10,6 @@ "Idempotency": "1.3.0", "BatchProcessing": "1.2.1", "EventHandler": "1.0.0", - "BedrockAgentFunctionResolver": "1.0.0-alpha.1", + "EventHandler.Resolvers.BedrockAgentFunction": "1.0.0-alpha.1", } } From 5c4b2402f51f53bc77ed90f1a353c35b8a049dee Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 May 2025 12:27:43 +0100 Subject: [PATCH 20/52] docs: update documentation for Bedrock Agent Function Resolver; enhance error handling and session attributes management --- .../event_handler/bedrock_agent_function.md | 216 ++++++++++++++---- ...dler.Resolvers.BedrockAgentFunction.csproj | 1 - .../Readme.md | 122 +++++++--- ...ockAgentFunctionResolverAdditionalTests.cs | 128 +++++++++++ 4 files changed, 394 insertions(+), 73 deletions(-) diff --git a/docs/core/event_handler/bedrock_agent_function.md b/docs/core/event_handler/bedrock_agent_function.md index 0b2b866e..2889406d 100644 --- a/docs/core/event_handler/bedrock_agent_function.md +++ b/docs/core/event_handler/bedrock_agent_function.md @@ -6,31 +6,71 @@ description: Event Handler - Bedrock Agent Function Resolver # 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](https://docs.aws.amazon.com/bedrock/latest/userguide/agents.html#agents-how) and focus on building your agent's logic without worrying about parsing and routing requests. + +```mermaid +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 - **Simple Tool Registration**: Register functions with descriptive names that Bedrock Agents can invoke - **Automatic Parameter Handling**: Parameters are automatically extracted from Bedrock Agent requests and converted to the appropriate types -- **Type Safety**: Strongly typed parameters and return values -- **Multiple Return Types**: Support for returning strings, primitive types, objects, or custom types -- **Flexible Input Options**: Support for various parameter types including string, int, bool, DateTime, and enums - **Lambda Context Access**: Easy access to Lambda context for logging and AWS Lambda features - **Dependency Injection Support**: Seamless integration with .NET's dependency injection system +- **AOT Compatibility**: 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: ```bash -dotnet add package AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver +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. + +??? note "Click to see example IaC templates" + ## Basic Usage -Here's a simple example showing how to register and use tool functions: +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. ```csharp using Amazon.BedrockAgentRuntime.Model; @@ -88,7 +128,7 @@ _resolver.Tool( ### Accessing Lambda Context -Access the Lambda context in your functions: +You can access to the original Lambda event or context for additional information. These are passed to the handler function as optional arguments. ```csharp _resolver.Tool( @@ -101,32 +141,85 @@ _resolver.Tool( }); ``` -### Working with Complex Return Types +### 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. -Return complex objects that will be converted to appropriate responses: +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. ```csharp -public class WeatherReport +resolver.Tool("CustomFailure", () => { - public string City { get; set; } - public string Conditions { get; set; } - public int Temperature { get; set; } - - public override string ToString() + // Return a custom FAILURE response + return new BedrockFunctionResponse { - return $"Weather in {City}: {Conditions}, {Temperature}°F"; + 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. + +```csharp +// 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(request.SessionAttributes ?? new Dictionary()) + { + ["counter"] = currentCount.ToString(), + ["lastAccessed"] = DateTime.UtcNow.ToString("o") + }; -_resolver.Tool( - "GetDetailedWeather", - "Returns detailed weather information for a location", - (string city) => new WeatherReport - { - City = city, - Conditions = "Partly Cloudy", - Temperature = 72 - }); + // 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 @@ -198,33 +291,70 @@ resolver.Tool( 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: - - Finds the registered handler for the requested function - - Extracts and converts parameters to the correct types - - Invokes your handler with the parameters - - Formats the response in the way Bedrock Agents expect + - Finds the registered handler for the requested function + - Extracts and converts parameters to the correct types + - Invokes your handler with the parameters + - Formats the response in the way Bedrock Agents expect 5. The agent receives the response and uses it to continue the conversation with the user ## Supported Parameter Types - `string` -- `int` / `long` -- `double` / `decimal` +- `int` +- `number` - `bool` -- `DateTime` -- `Guid` - `enum` types - `ILambdaContext` (for accessing Lambda context) - `ActionGroupInvocationInput` (for accessing raw request) - Any service registered in dependency injection -## Benefits -- **Reduced Boilerplate**: Eliminate repetitive code for parsing requests and formatting responses -- **Type Safety**: Strong typing for parameters and return values -- **Simplified Development**: Focus on business logic instead of request/response handling -- **Reusable Components**: Build a library of tool functions that can be shared across agents -- **Easy Testing**: Functions can be easily unit tested in isolation -- **Flexible Integration**: Works seamlessly with AWS Lambda and Bedrock Agents +## 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 + +```csharp +// 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: + +```csharp + +var services = new ServiceCollection(); +services.AddSingleton(); +services.AddBedrockResolver(); // Extension method to register the resolver + +var serviceProvider = services.BuildServiceProvider(); +var resolver = serviceProvider.GetRequiredService() + .RegisterTool(); // Register tools from the class during service registration + +``` ## Complete Example with Dependency Injection @@ -300,8 +430,4 @@ namespace MyBedrockAgent } } } -``` - -## Learn More - -For more information about Amazon Bedrock Agents and function integration, see the [Amazon Bedrock documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-tools.html). +``` \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj index 1f5c2aea..e6cbf62a 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj @@ -9,7 +9,6 @@ enable enable true - 1.0.4 diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md index d1017d76..d58dce93 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md @@ -9,25 +9,40 @@ Amazon Bedrock Agents can invoke functions to perform tasks based on user input. - **Simple Tool Registration**: Register functions with descriptive names that Bedrock Agents can invoke - **Automatic Parameter Handling**: Parameters are automatically extracted from Bedrock Agent requests and converted to the appropriate types -- **Type Safety**: Strongly typed parameters and return values -- **Multiple Return Types**: Support for returning strings, primitive types, objects, or custom types -- **Flexible Input Options**: Support for various parameter types including string, int, bool, DateTime, and enums - **Lambda Context Access**: Easy access to Lambda context for logging and AWS Lambda features - **Dependency Injection Support**: Seamless integration with .NET's dependency injection system -- **Error Handling**: Automatic error capturing and formatting for responses -- **Async Support**: First-class support for asynchronous function execution +- **AOT Compatibility**: 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: ```bash -dotnet add package AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver +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. + +??? note "Click to see example IaC templates" + ## Basic Usage -Here's a simple example showing how to register and use tool functions: +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. ```csharp using Amazon.BedrockAgentRuntime.Model; @@ -85,7 +100,7 @@ _resolver.Tool( ### Accessing Lambda Context -Access the Lambda context in your functions: +You can access to the original Lambda event or context for additional information. These are passed to the handler function as optional arguments. ```csharp _resolver.Tool( @@ -98,32 +113,85 @@ _resolver.Tool( }); ``` -### Working with Complex Return Types +### 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. -Return complex objects that will be converted to appropriate responses: +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. ```csharp -public class WeatherReport +resolver.Tool("CustomFailure", () => { - public string City { get; set; } - public string Conditions { get; set; } - public int Temperature { get; set; } - - public override string ToString() + // 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. + +```csharp +// 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)) { - return $"Weather in {City}: {Conditions}, {Temperature}°F"; + currentCount = count; } -} + + // Increment the counter + currentCount++; + + // Create a new dictionary with updated counter + var updatedSessionAttributes = new Dictionary(request.SessionAttributes ?? new Dictionary()) + { + ["counter"] = currentCount.ToString(), + ["lastAccessed"] = DateTime.UtcNow.ToString("o") + }; -_resolver.Tool( - "GetDetailedWeather", - "Returns detailed weather information for a location", - (string city) => new WeatherReport - { - City = city, - Conditions = "Partly Cloudy", - Temperature = 72 - }); + // 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 diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverAdditionalTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverAdditionalTests.cs index 62d09fde..3f73c686 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverAdditionalTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverAdditionalTests.cs @@ -200,6 +200,134 @@ public void Tool_WithBedrockFunctionResponseHandler_MapsCorrectly() // Assert Assert.Equal("Direct response", result.Response.FunctionResponse.ResponseBody.Text.Body); } + + [Fact] + public void Tool_WithCustomFailureResponse_ReturnsFailureState() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + 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 + } + } + }; + }); + + var input = new BedrockFunctionRequest { Function = "CustomFailure" }; + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + Assert.Equal("Critical error occurred: Database unavailable", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("FAILURE", result.Response.FunctionResponse.ResponseState.ToString()); + } + + [Fact] + public void Tool_WithSessionAttributesPersistence_MaintainsStateAcrossInvocations() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + + // 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(request.SessionAttributes ?? new Dictionary()) + { + ["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 + }; + }); + + // First invocation - should start with 0 and increment to 1 + var firstInput = new BedrockFunctionRequest + { + Function = "CounterTool", + SessionAttributes = new Dictionary(), + PromptSessionAttributes = new Dictionary { ["prompt"] = "initial" } + }; + + // Second invocation - should use the session attributes from first response + var secondInput = new BedrockFunctionRequest { Function = "CounterTool" }; + + // Act + var firstResult = resolver.Resolve(firstInput); + // In a real scenario, the agent would pass the updated session attributes back to us + secondInput.SessionAttributes = firstResult.SessionAttributes; + secondInput.PromptSessionAttributes = firstResult.PromptSessionAttributes; + var secondResult = resolver.Resolve(secondInput); + + // Now a third invocation to verify the counter keeps incrementing + var thirdInput = new BedrockFunctionRequest { Function = "CounterTool" }; + thirdInput.SessionAttributes = secondResult.SessionAttributes; + thirdInput.PromptSessionAttributes = secondResult.PromptSessionAttributes; + var thirdResult = resolver.Resolve(thirdInput); + + // Assert + Assert.Equal("Current count: 1", firstResult.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("Current count: 2", secondResult.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("Current count: 3", thirdResult.Response.FunctionResponse.ResponseBody.Text.Body); + + // Verify session attributes are maintained + Assert.Equal("1", firstResult.SessionAttributes["counter"]); + Assert.Equal("2", secondResult.SessionAttributes["counter"]); + Assert.Equal("3", thirdResult.SessionAttributes["counter"]); + + // Verify prompt attributes are preserved + Assert.Equal("initial", firstResult.PromptSessionAttributes["prompt"]); + Assert.Equal("initial", secondResult.PromptSessionAttributes["prompt"]); + Assert.Equal("initial", thirdResult.PromptSessionAttributes["prompt"]); + } private class TestObject { From 07f1e220de84ff7a3958083417b0c5a7024858b6 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 May 2025 12:29:01 +0100 Subject: [PATCH 21/52] chore: update BedrockAgentFunction version to 1.0.0-alpha.2 in version.json --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 53805977..92675f91 100644 --- a/version.json +++ b/version.json @@ -10,6 +10,6 @@ "Idempotency": "1.3.0", "BatchProcessing": "1.2.1", "EventHandler": "1.0.0", - "EventHandler.Resolvers.BedrockAgentFunction": "1.0.0-alpha.1", + "EventHandler.Resolvers.BedrockAgentFunction": "1.0.0-alpha.2" } } From 3c4f0121aac1ec2e16f3714a803cdfd4a4621b71 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 2 Jun 2025 09:59:25 +0100 Subject: [PATCH 22/52] docs: update documentation for Bedrock Agent Function Resolver; add TODO for CDK integration and remove redundant resource requirements --- docs/core/event_handler/bedrock_agent_function.md | 3 +++ .../Readme.md | 6 ------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/core/event_handler/bedrock_agent_function.md b/docs/core/event_handler/bedrock_agent_function.md index 2889406d..f9acae45 100644 --- a/docs/core/event_handler/bedrock_agent_function.md +++ b/docs/core/event_handler/bedrock_agent_function.md @@ -68,6 +68,9 @@ You must create an Amazon Bedrock Agent with at least one action group. Each act ??? note "Click to see example IaC templates" +TODO: add cdk + + ## 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. diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md index d58dce93..9904f64e 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md @@ -34,12 +34,6 @@ Install the package via NuGet: 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. - -??? note "Click to see example IaC templates" - ## 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. From 8fece716b52818d1bd459f84bfbcebf414347fa6 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:51:43 +0100 Subject: [PATCH 23/52] feat: enhance BedrockAgentFunctionResolver with custom type support and JSON serialization options --- .../BedrockAgentFunctionResolver.cs | 12 ++- .../BedrockAgentFunctionResolverExtensions.cs | 19 +++-- .../DiBedrockAgentFunctionResolver.cs | 6 +- .../Helpers/ParameterMapper.cs | 79 ++++++++++++++++--- .../BedrockAgentFunctionResolverTests.cs | 68 ++++++++++++++++ 5 files changed, 164 insertions(+), 20 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs index a4ece182..d24cf62c 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs @@ -13,8 +13,7 @@ * permissions and limitations under the License. */ -using System.Globalization; -using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using Amazon.Lambda.Core; using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers; @@ -48,6 +47,15 @@ private readonly private readonly ResultConverter _resultConverter = new(); private readonly ParameterMapper _parameterMapper = new(); + /// + /// Initializes a new instance of the class. + /// Optionally accepts a type resolver for JSON serialization. + /// + public BedrockAgentFunctionResolver(IJsonTypeInfoResolver? typeResolver = null) + { + _parameterMapper = new ParameterMapper(typeResolver); + } + /// /// Checks if another tool can be registered, and logs a warning if the maximum limit is reached /// or if a tool with the same name is already registered diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs index bd2c8cbc..307152a3 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs @@ -16,6 +16,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reflection; +using System.Text.Json.Serialization.Metadata; using Microsoft.Extensions.DependencyInjection; // ReSharper disable once CheckNamespace @@ -30,6 +31,7 @@ public static class BedrockResolverExtensions /// Registers a Bedrock Agent Function Resolver with dependency injection support. /// /// The service collection to add the resolver to. + /// /// The updated service collection. /// /// @@ -41,10 +43,12 @@ public static class BedrockResolverExtensions /// } /// /// - public static IServiceCollection AddBedrockResolver(this IServiceCollection services) + public static IServiceCollection AddBedrockResolver( + this IServiceCollection services, + IJsonTypeInfoResolver? typeResolver = null) { services.AddSingleton(sp => - new DiBedrockAgentFunctionResolver(sp)); + new DiBedrockAgentFunctionResolver(sp, typeResolver)); return services; } @@ -72,7 +76,8 @@ public static IServiceCollection AddBedrockResolver(this IServiceCollection serv /// resolver.RegisterTool<WeatherTools>(); /// /// - public static BedrockAgentFunctionResolver RegisterTool<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>( + public static BedrockAgentFunctionResolver RegisterTool< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>( this BedrockAgentFunctionResolver resolver) where T : class { @@ -89,14 +94,14 @@ public static IServiceCollection AddBedrockResolver(this IServiceCollection serv if (attr == null) continue; string toolName = attr.Name ?? method.Name; - string description = attr.Description ?? + string description = attr.Description ?? string.Empty; // Create delegate from the static method var del = Delegate.CreateDelegate( - GetDelegateType(method), + GetDelegateType(method), method); - + // Call the Tool method directly instead of using reflection resolver.Tool(toolName, description, del); } @@ -109,7 +114,7 @@ private static Type GetDelegateType(MethodInfo method) var parameters = method.GetParameters(); var parameterTypes = parameters.Select(p => p.ParameterType).ToList(); parameterTypes.Add(method.ReturnType); - + return Expression.GetDelegateType(parameterTypes.ToArray()); } } diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/DiBedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/DiBedrockAgentFunctionResolver.cs index 0d893623..82064d43 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/DiBedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/DiBedrockAgentFunctionResolver.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization.Metadata; + namespace AWS.Lambda.Powertools.EventHandler.Resolvers; /// @@ -14,7 +16,9 @@ internal class DiBedrockAgentFunctionResolver : BedrockAgentFunctionResolver /// Initializes a new instance of the class. /// /// The service provider for dependency injection. - public DiBedrockAgentFunctionResolver(IServiceProvider serviceProvider) + /// + public DiBedrockAgentFunctionResolver(IServiceProvider serviceProvider, IJsonTypeInfoResolver? typeResolver = null) + : base(typeResolver) { ServiceProvider = serviceProvider; } diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs index 1c9edf77..1f723d98 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs @@ -15,6 +15,7 @@ using System.Reflection; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using Amazon.Lambda.Core; using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; @@ -26,7 +27,13 @@ namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Help internal class ParameterMapper { private readonly ParameterTypeValidator _validator = new(); - + private readonly IJsonTypeInfoResolver? _typeResolver; + + public ParameterMapper(IJsonTypeInfoResolver? typeResolver = null) + { + _typeResolver = typeResolver; + } + /// /// Maps parameters for a handler method from a Bedrock function request /// @@ -36,15 +43,14 @@ internal class ParameterMapper /// Optional service provider for dependency injection /// Array of arguments to pass to the handler public object?[] MapParameters( - MethodInfo methodInfo, - BedrockFunctionRequest input, + MethodInfo methodInfo, + BedrockFunctionRequest input, ILambdaContext? context, IServiceProvider? serviceProvider) { var parameters = methodInfo.GetParameters(); var args = new object?[parameters.Length]; var accessor = new ParameterAccessor(input.Parameters); - var bedrockParamIndex = 0; for (var i = 0; i < parameters.Length; i++) { @@ -54,15 +60,66 @@ internal class ParameterMapper if (paramType == typeof(ILambdaContext)) { args[i] = context; + continue; // Skip further processing for this parameter } else if (paramType == typeof(BedrockFunctionRequest)) { args[i] = input; + continue; // Skip further processing for this parameter + } + + // Try to deserialize custom complex type from InputText + if (!string.IsNullOrEmpty(input.InputText) && + !paramType.IsPrimitive && + paramType != typeof(string) && + !paramType.IsEnum) + { + try + { + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + if (_typeResolver != null) + { + options.TypeInfoResolver = _typeResolver; + + // Get the JsonTypeInfo for the parameter type + var jsonTypeInfo = _typeResolver.GetTypeInfo(paramType, options); + if (jsonTypeInfo != null) + { + // Use the AOT-friendly overload with JsonTypeInfo + args[i] = JsonSerializer.Deserialize(input.InputText, jsonTypeInfo); + + if (args[i] != null) + { + continue; + } + } + } + else + { + // Fallback to non-AOT deserialization with warning +#pragma warning disable IL2026, IL3050 + args[i] = JsonSerializer.Deserialize(input.InputText, paramType, options); +#pragma warning restore IL2026, IL3050 + + if (args[i] != null) + { + continue; + } + } + } + catch + { + // Deserialization failed, continue to regular parameter mapping + } } - else if (_validator.IsBedrockParameter(paramType)) + + if (_validator.IsBedrockParameter(paramType)) { - args[i] = MapBedrockParameter(paramType, parameter.Name ?? $"arg{bedrockParamIndex}", accessor); - bedrockParamIndex++; + args[i] = MapBedrockParameter(paramType, parameter.Name ?? $"arg{i}", accessor); } else if (serviceProvider != null) { @@ -107,9 +164,11 @@ internal class ParameterMapper if (paramType == typeof(double[])) return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.DoubleArray); if (paramType == typeof(bool[])) - return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.BooleanArray); + return JsonSerializer.Deserialize(jsonArrayStr, + BedrockFunctionResolverContext.Default.BooleanArray); if (paramType == typeof(decimal[])) - return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.DecimalArray); + return JsonSerializer.Deserialize(jsonArrayStr, + BedrockFunctionResolverContext.Default.DecimalArray); } catch (JsonException) { @@ -147,4 +206,4 @@ internal class ParameterMapper return null; } } -} +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverTests.cs index 5e630214..1090a248 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverTests.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Text; +using System.Text.Json.Serialization; using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.EventHandler.Resolvers; @@ -800,6 +801,62 @@ public void TestToolOverrideWithWarning() Assert.Equal("New Calculator", result.Response.FunctionResponse.ResponseBody.Text.Body); } + [Fact] + public void TestFunctionHandlerWithCustomType() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + 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)}"; + } + ); + + var input = new BedrockFunctionRequest + { + Function = "PriceCalculator", + InputText = "{\"Price\": 29.99}", // JSON representation of MyCustomType + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains("35.99", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void TestFunctionHandlerWithCustomTypeWithTypeInfoResolver() + { + // Arrange + 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)}"; + } + ); + + var input = new BedrockFunctionRequest + { + Function = "PriceCalculator", + InputText = "{\"Price\": 29.99}", // JSON representation of MyCustomType + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains("35.99", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + [Fact] public void TestAttributeBasedToolRegistration() { @@ -873,3 +930,14 @@ public async Task DoSomething(string location, int days) return await Task.FromResult($"Forecast for {location} for {days} days"); } } + +public class MyCustomType +{ + public decimal Price { get; set; } +} + + +[JsonSerializable(typeof(MyCustomType))] +public partial class MycustomSerializationContext : JsonSerializerContext +{ +} \ No newline at end of file From 890cf7d272095a1a30d848605ad992ed3517dc2c Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:53:43 +0100 Subject: [PATCH 24/52] Update README.md Signed-off-by: Henrique Graca <999396+hjgraca@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5946eb4c..8dd862ad 100644 --- a/README.md +++ b/README.md @@ -92,8 +92,8 @@ Knowing which companies are using this library is important to help prioritize t The following companies, among others, use Powertools: * [Caylent](https://caylent.com/) -* [Pushpay](https://pushpay.com/) * [Instil Software](https://instil.co/) +* [Pushpay](https://pushpay.com/) ### Sharing your work From 81ab4066094867b6ac4396726c1eaba9e1f8d252 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:57:23 +0100 Subject: [PATCH 25/52] Update index.md Signed-off-by: Henrique Graca <999396+hjgraca@users.noreply.github.com> --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 08a46cac..68251d62 100644 --- a/docs/index.md +++ b/docs/index.md @@ -163,10 +163,10 @@ Knowing which companies are using this library is important to help prioritize t [**Caylent**](https://caylent.com/){target="_blank" rel="nofollow"} { .card } -[**Pushpay**](https://pushpay.com/){target="_blank" rel="nofollow"} +[**Instil Software**](https://instil.co/){target="_blank" rel="nofollow"} { .card } -[**Instil Software**](https://instil.co/){target="_blank" rel="nofollow"} +[**Pushpay**](https://pushpay.com/){target="_blank" rel="nofollow"} { .card } From 7d89d718aee9cf3e25dd5e3137806225ceed1f1e Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 22 May 2025 12:29:29 +0100 Subject: [PATCH 26/52] bedrock agent function first commit --- libraries/AWS.Lambda.Powertools.sln | 15 ++ ...andler.BedrockAgentFunctionResolver.csproj | 20 ++ .../BedrockAgentFunctionResolver.cs | 104 ++++++++ .../InternalsVisibleTo.cs | 18 ++ .../Readme.md | 57 +++++ libraries/src/Directory.Packages.props | 1 + ...ambda.Powertools.EventHandler.Tests.csproj | 5 + .../BedrockAgentFunctionResolverTests.cs | 232 ++++++++++++++++++ .../bedrockFunctionEvent.json | 27 ++ 9 files changed, 479 insertions(+) create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/InternalsVisibleTo.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent.json diff --git a/libraries/AWS.Lambda.Powertools.sln b/libraries/AWS.Lambda.Powertools.sln index 5d7cd4f9..0ca62f24 100644 --- a/libraries/AWS.Lambda.Powertools.sln +++ b/libraries/AWS.Lambda.Powertools.sln @@ -109,6 +109,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Event EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.EventHandler", "src\AWS.Lambda.Powertools.EventHandler\AWS.Lambda.Powertools.EventHandler.csproj", "{F4B8D5AF-D3CA-4910-A14D-E5BAEF0FD1DE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver", "src\AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver\AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj", "{281F7EB5-ACE5-458F-BC88-46A8899DF3BA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -590,6 +592,18 @@ Global {F4B8D5AF-D3CA-4910-A14D-E5BAEF0FD1DE}.Release|x64.Build.0 = Release|Any CPU {F4B8D5AF-D3CA-4910-A14D-E5BAEF0FD1DE}.Release|x86.ActiveCfg = Release|Any CPU {F4B8D5AF-D3CA-4910-A14D-E5BAEF0FD1DE}.Release|x86.Build.0 = Release|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Debug|x64.ActiveCfg = Debug|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Debug|x64.Build.0 = Debug|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Debug|x86.ActiveCfg = Debug|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Debug|x86.Build.0 = Debug|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|Any CPU.Build.0 = Release|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|x64.ActiveCfg = Release|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|x64.Build.0 = Release|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|x86.ActiveCfg = Release|Any CPU + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution @@ -641,5 +655,6 @@ Global {7FC6DD65-0352-4139-8D08-B25C0A0403E3} = {4EAB66F9-C9CB-4E8A-BEE6-A14CD7FDE02F} {61374D8E-F77C-4A31-AE07-35DAF1847369} = {1CFF5568-8486-475F-81F6-06105C437528} {F4B8D5AF-D3CA-4910-A14D-E5BAEF0FD1DE} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5} + {281F7EB5-ACE5-458F-BC88-46A8899DF3BA} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5} EndGlobalSection EndGlobal diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj new file mode 100644 index 00000000..a392b395 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj @@ -0,0 +1,20 @@ + + + + + AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver + Powertools for AWS Lambda (.NET) - Event Handler Bedrock Agent Function Resolver package. + AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver + AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver + net8.0 + false + enable + enable + + + + + + + + \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs new file mode 100644 index 00000000..946b363c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs @@ -0,0 +1,104 @@ +using Amazon.BedrockAgentRuntime.Model; +using Amazon.Lambda.Core; + +// ReSharper disable once CheckNamespace +namespace AWS.Lambda.Powertools.EventHandler +{ + /// + /// Resolver for Amazon Bedrock Agent functions. + /// Routes function calls to appropriate handlers based on function name. + /// + public class BedrockAgentFunctionResolver + { + private readonly Dictionary> _handlers = new(); + + /// + /// Registers a handler for a tool function without parameters. + /// + /// The function name to handle + /// Function handler without parameters + /// Optional description of the function + public BedrockAgentFunctionResolver Tool(string name, Func handler, string description = "") + { + _handlers[name] = (_, _) => handler(); + return this; + } + + /// + /// Registers a handler for a tool function with input. + /// + /// The function name to handle + /// Function handler with input + /// Optional description of the function + public BedrockAgentFunctionResolver Tool(string name, Func handler, string description = "") + { + _handlers[name] = (input, _) => handler(input); + return this; + } + + /// + /// Registers a handler for a tool function with input and context. + /// + /// The function name to handle + /// Function handler with input and context + /// Optional description of the function + public BedrockAgentFunctionResolver Tool(string name, Func handler, string description = "") + { + _handlers[name] = (input, context) => handler(input, context ?? throw new ArgumentNullException(nameof(context))); + return this; + } + + /// + /// Resolves and processes a Bedrock Agent function invocation. + /// + /// The function invocation input + /// Lambda execution context + public ActionGroupInvocationOutput Resolve(ActionGroupInvocationInput input, ILambdaContext? context = null) + { + return ResolveAsync(input, context).GetAwaiter().GetResult(); + } + + /// + /// Asynchronously resolves and processes a Bedrock Agent function invocation. + /// + /// The function invocation input + /// Lambda execution context + public async Task ResolveAsync(ActionGroupInvocationInput input, ILambdaContext? context = null) + { + return await Task.FromResult(HandleEvent(input, context)); + } + + private ActionGroupInvocationOutput HandleEvent(ActionGroupInvocationInput input, ILambdaContext? context) + { + if (string.IsNullOrEmpty(input.Function)) + { + return new ActionGroupInvocationOutput + { + Text = "No function specified in the request" + }; + } + + if (_handlers.TryGetValue(input.Function, out var handler)) + { + try + { + return handler(input, context); + } + catch (Exception ex) + { + context?.Logger.LogError($"Error executing function {input.Function}: {ex.Message}"); + return new ActionGroupInvocationOutput + { + Text = $"Error executing function: {ex.Message}" + }; + } + } + + context?.Logger.LogWarning($"No handler registered for function: {input.Function}"); + return new ActionGroupInvocationOutput + { + Text = $"No handler registered for function: {input.Function}" + }; + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/InternalsVisibleTo.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/InternalsVisibleTo.cs new file mode 100644 index 00000000..9e952373 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/InternalsVisibleTo.cs @@ -0,0 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.EventHandler.Tests")] \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md new file mode 100644 index 00000000..08947c02 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md @@ -0,0 +1,57 @@ +# AWS Lambda Powertools for .NET - Bedrock Agent Function Resolver + +## Overview +The Bedrock Agent Function Resolver is a custom function resolver for AWS Lambda Powertools for .NET. It is designed to work with the Bedrock Agent, a tool that simplifies the process of building and deploying serverless applications on AWS Lambda. +The Bedrock Agent Function Resolver allows you to easily resolve and invoke Lambda functions using the Bedrock Agent's conventions and best practices. +This custom function resolver is part of the AWS Lambda Powertools for .NET library, which provides a suite of utilities for building serverless applications on AWS Lambda. +## Features +- Custom function resolver for AWS Lambda Powertools for .NET +- Supports Bedrock Agent conventions and best practices +- Simplifies the process of resolving and invoking Lambda functions +- Integrates with AWS Lambda Powertools for .NET library +- Supports dependency injection and configuration +- Provides a consistent and easy-to-use API for resolving functions +- Supports asynchronous and synchronous function invocation +- Supports error handling and logging +- Supports custom serialization and deserialization +- Supports custom middleware and filters + +## Getting Started +To get started with the Bedrock Agent Function Resolver, you need to install the AWS Lambda Powertools for .NET library and the Bedrock Agent Function Resolver package. You can do this using NuGet: + +```bash +dotnet add package AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver +``` +## Usage +To use the Bedrock Agent Function Resolver, you need to create an instance of the `BedrockAgentFunctionResolver` class and register it with the AWS Lambda Powertools for .NET library. You can do this in your Lambda function's entry point: + +```csharp +using Amazon.Lambda.Core; +using Amazon.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver; + + +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + +namespace MyLambdaFunction +{ + public class Function + { + private readonly BedrockAgentFunctionResolver _functionResolver; + + public Function() + { + // Create an instance of the Bedrock Agent Function Resolver + _functionResolver = new BedrockAgentFunctionResolver(); + } + + public async Task FunctionHandler(ILambdaContext context) + { + // Use the function resolver to resolve and invoke a Lambda function + var result = await _functionResolver.ResolveAndInvokeAsync("MyLambdaFunctionName", new { /* input parameters */ }); + + // Process the result + context.Logger.LogLine($"Result: {result}"); + } + } +} +``` \ No newline at end of file diff --git a/libraries/src/Directory.Packages.props b/libraries/src/Directory.Packages.props index db4a6a7f..f9a4730c 100644 --- a/libraries/src/Directory.Packages.props +++ b/libraries/src/Directory.Packages.props @@ -9,6 +9,7 @@ + diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj index eef47181..aab78861 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj @@ -32,6 +32,7 @@ + @@ -40,6 +41,10 @@ PreserveNewest + + + PreserveNewest + diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs new file mode 100644 index 00000000..a406a15f --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs @@ -0,0 +1,232 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Amazon.BedrockAgentRuntime.Model; +using Amazon.Lambda.TestUtilities; + +namespace AWS.Lambda.Powertools.EventHandler.Tests; + +public class BedrockAgentFunctionResolverTests +{ + private readonly ActionGroupInvocationInput _bedrockEvent; + + public BedrockAgentFunctionResolverTests() + { + _bedrockEvent = JsonSerializer.Deserialize( + File.ReadAllText("bedrockFunctionEvent.json"), + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() } + })!; + } + + [Fact] + public void TestFunctionHandlerWithNoParameters() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + + var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + Assert.Equal("Hello, World!", result.Text); + } + + [Fact] + public async Task TestFunctionHandlerWithNoParametersAsync() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + + var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var context = new TestLambdaContext(); + + // Act + var result = await resolver.ResolveAsync(input, context); + + // Assert + Assert.Equal("Hello, World!", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithDescription() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }, + "This is a test function"); + + var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + Assert.Equal("Hello, World!", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithMultiplTools() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction1", () => new ActionGroupInvocationOutput { Text = "Hello from Function 1!" }); + resolver.Tool("TestFunction2", () => new ActionGroupInvocationOutput { Text = "Hello from Function 2!" }); + + var input1 = new ActionGroupInvocationInput { Function = "TestFunction1" }; + var input2 = new ActionGroupInvocationInput { Function = "TestFunction2" }; + var context = new TestLambdaContext(); + + // Act + var result1 = resolver.Resolve(input1, context); + var result2 = resolver.Resolve(input2, context); + + // Assert + Assert.Equal("Hello from Function 1!", result1.Text); + Assert.Equal("Hello from Function 2!", result2.Text); + } + + + [Fact] + public void TestFunctionHandlerWithInput() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction", + (input) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); + + var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + Assert.Equal("Hello, TestFunction!", result.Text); + } + + [Fact] + public async Task TestFunctionHandlerWithInputAsync() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction", + (input) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); + + var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var context = new TestLambdaContext(); + + // Act + var result = await resolver.ResolveAsync(input, context); + + // Assert + Assert.Equal("Hello, TestFunction!", result.Text); + } + + [Fact] + public void TestFunctionHandlerNoToolMatch() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + + var input = new ActionGroupInvocationInput { Function = "NonExistentFunction" }; + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + Assert.Equal("No handler registered for function: NonExistentFunction", result.Text); + } + + [Fact] + public async Task TestFunctionHandlerNoToolMatchAsync() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + + var input = new ActionGroupInvocationInput { Function = "NonExistentFunction" }; + var context = new TestLambdaContext(); + + // Act + var result = await resolver.ResolveAsync(input, context); + + // Assert + Assert.Equal("No handler registered for function: NonExistentFunction", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithParameters() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + + var input = new ActionGroupInvocationInput + { + Function = "TestFunction", + RequestBody = new RequestBody + { + + }, + Parameters = new List + { + new Parameter + { + Name = "a", + Value = "1", + Type = "Number" + }, + new Parameter + { + Name = "b", + Value = "1", + Type = "Number" + } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + Assert.Equal("Hello, World!", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithEvent() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("sum_numbers", (payload, context ) => + { + + return new ActionGroupInvocationOutput { Text = "2" }; + }); + + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(_bedrockEvent, context); + + // Assert + Assert.Equal("2", result.Text); + } +} + +// Types +// String +// Number +// Integer +// Boolean +// Array \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent.json b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent.json new file mode 100644 index 00000000..f2cedeb1 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent.json @@ -0,0 +1,27 @@ +{ + "messageVersion": "1.0", + "function": "sum_numbers", + "sessionId": "455081292773641", + "agent": { + "name": "powertools-test", + "version": "DRAFT", + "id": "WPMRGAPAPJ", + "alias": "TSTALIASID" + }, + "parameters": [ + { + "name": "a", + "type": "number", + "value": "1" + }, + { + "name": "b", + "type": "number", + "value": "1" + } + ], + "actionGroup": "utility-tasks", + "sessionAttributes": {}, + "promptSessionAttributes": {}, + "inputText": "Sum 1 and 1" +} \ No newline at end of file From df2e42275d07ab072c222261418c8752d023175f Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Sun, 25 May 2025 16:18:00 +0100 Subject: [PATCH 27/52] add methods that take DI services, match parameters with handler arguments --- ...andler.BedrockAgentFunctionResolver.csproj | 2 + .../BedrockAgentFunctionResolver.cs | 301 ++++++++++++++++-- .../BedrockAgentFunctionResolverExtensions.cs | 27 ++ .../ParameterAccessor.cs | 126 ++++++++ libraries/src/Directory.Packages.props | 1 + .../BedrockAgentFunctionResolverTests.cs | 170 +++++++++- 6 files changed, 589 insertions(+), 38 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/ParameterAccessor.cs diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj index a392b395..87033029 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj @@ -10,11 +10,13 @@ false enable enable + true + \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs index 946b363c..5f33caa9 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs @@ -1,58 +1,254 @@ -using Amazon.BedrockAgentRuntime.Model; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; -// ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler { - /// - /// Resolver for Amazon Bedrock Agent functions. - /// Routes function calls to appropriate handlers based on function name. - /// public class BedrockAgentFunctionResolver { - private readonly Dictionary> _handlers = new(); + private readonly + Dictionary> + _handlers = new(); + + private static readonly HashSet _bedrockParameterTypes = new() + { + typeof(string), + typeof(int), + typeof(long), + typeof(double), + typeof(bool), + typeof(decimal), + typeof(DateTime), + typeof(Guid) + }; + + private static bool IsBedrockParameter(Type type) => + _bedrockParameterTypes.Contains(type) || type.IsEnum; /// - /// Registers a handler for a tool function without parameters. + /// Registers a handler that directly accepts ActionGroupInvocationInput and returns ActionGroupInvocationOutput /// - /// The function name to handle - /// Function handler without parameters - /// Optional description of the function - public BedrockAgentFunctionResolver Tool(string name, Func handler, string description = "") + public BedrockAgentFunctionResolver Tool( + string name, + Func handler, + string description = "") { - _handlers[name] = (_, _) => handler(); + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + _handlers[name] = handler; return this; } /// - /// Registers a handler for a tool function with input. + /// Registers a handler that directly accepts ActionGroupInvocationInput and returns ActionGroupInvocationOutput /// - /// The function name to handle - /// Function handler with input - /// Optional description of the function - public BedrockAgentFunctionResolver Tool(string name, Func handler, string description = "") + public BedrockAgentFunctionResolver Tool( + string name, + Func handler, + string description = "") { + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + _handlers[name] = (input, _) => handler(input); return this; } /// - /// Registers a handler for a tool function with input and context. + /// Registers a handler for a tool function with automatically converted return type. + /// + public BedrockAgentFunctionResolver Tool( + string name, + string description = "", + Delegate? handler = null) + { + // Delegate to the generic version with object as return type + return Tool(name, description, handler); + } + + /// + /// Registers a handler for a tool function with typed return value. /// - /// The function name to handle - /// Function handler with input and context - /// Optional description of the function - public BedrockAgentFunctionResolver Tool(string name, Func handler, string description = "") + public BedrockAgentFunctionResolver Tool( + string name, + string description = "", + Delegate? handler = null) { - _handlers[name] = (input, context) => handler(input, context ?? throw new ArgumentNullException(nameof(context))); + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + _handlers[name] = (input, context) => + { + var accessor = new ParameterAccessor(input.Parameters); + var parameters = handler.Method.GetParameters(); + var args = new object?[parameters.Length]; + var bedrockParamIndex = 0; + + // Get service provider from resolver if available + var serviceProvider = (this as DIBedrockAgentFunctionResolver)?.ServiceProvider; + + // Map parameters from Bedrock input and DI + for (var i = 0; i < parameters.Length; i++) + { + var parameter = parameters[i]; + var paramType = parameter.ParameterType; + + if (paramType == typeof(ILambdaContext)) + { + args[i] = context; + } + else if (paramType == typeof(ActionGroupInvocationInput)) + { + args[i] = input; + } + else if (IsBedrockParameter(paramType)) + { + var paramName = parameter.Name ?? $"arg{bedrockParamIndex}"; + + // AOT-compatible parameter access - direct type checks + if (paramType == typeof(string)) + args[i] = accessor.Get(paramName); + else if (paramType == typeof(int)) + args[i] = accessor.Get(paramName); + else if (paramType == typeof(long)) + args[i] = accessor.Get(paramName); + else if (paramType == typeof(double)) + args[i] = accessor.Get(paramName); + else if (paramType == typeof(bool)) + args[i] = accessor.Get(paramName); + else if (paramType == typeof(decimal)) + args[i] = accessor.Get(paramName); + else if (paramType == typeof(DateTime)) + args[i] = accessor.Get(paramName); + else if (paramType == typeof(Guid)) + args[i] = accessor.Get(paramName); + else if (paramType.IsEnum) + { + // For enums, get as string and parse + var strValue = accessor.Get(paramName); + args[i] = !string.IsNullOrEmpty(strValue) ? Enum.Parse(paramType, strValue) : null; + } + + bedrockParamIndex++; + } + else if (serviceProvider != null) + { + // Resolve from DI + args[i] = serviceProvider.GetService(paramType); + } + } + + try + { + // Execute the handler + var result = handler.DynamicInvoke(args); + + // Direct return for ActionGroupInvocationOutput + if (result is ActionGroupInvocationOutput output) + return output; + + // Handle async results with specific type checks (AOT-compatible) + if (result is Task outputTask) + return outputTask.Result; + if (result is Task stringTask) + return ConvertToOutput((TResult)(object)stringTask.Result); + if (result is Task intTask) + return ConvertToOutput((TResult)(object)intTask.Result); + if (result is Task boolTask) + return ConvertToOutput((TResult)(object)boolTask.Result); + if (result is Task doubleTask) + return ConvertToOutput((TResult)(object)doubleTask.Result); + if (result is Task longTask) + return ConvertToOutput((TResult)(object)longTask.Result); + if (result is Task decimalTask) + return ConvertToOutput((TResult)(object)decimalTask.Result); + if (result is Task dateTimeTask) + return ConvertToOutput((TResult)(object)dateTimeTask.Result); + if (result is Task guidTask) + return ConvertToOutput((TResult)(object)guidTask.Result); + if (result is Task objectTask) + return ConvertToOutput((TResult)objectTask.Result!); + + // For regular Task with no result + if (result is Task task) + { + task.GetAwaiter().GetResult(); + return new ActionGroupInvocationOutput { Text = string.Empty }; + } + + return ConvertToOutput((TResult)result!); + } + catch (Exception ex) + { + context?.Logger.LogError($"Error executing function {name}: {ex.Message}"); + return new ActionGroupInvocationOutput + { + Text = $"Error executing function: {ex.Message}" + }; + } + }; + + return this; + } + + /// + /// Registers a parameter-less handler that returns ActionGroupInvocationOutput + /// + public BedrockAgentFunctionResolver Tool( + string name, + Func handler, + string description = "") + { + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + _handlers[name] = (input, context) => handler(); + return this; + } + + /// + /// Registers a parameter-less handler with automatic string conversion + /// + public BedrockAgentFunctionResolver Tool( + string name, + Func handler, + string description = "") + { + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + _handlers[name] = (input, context) => new ActionGroupInvocationOutput { Text = handler() }; + return this; + } + + /// + /// Registers a parameter-less handler with automatic object conversion + /// + public BedrockAgentFunctionResolver Tool( + string name, + Func handler, + string description = "") + { + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + _handlers[name] = (input, context) => + { + var result = handler(); + return ConvertToOutput(result); + }; return this; } /// /// Resolves and processes a Bedrock Agent function invocation. /// - /// The function invocation input - /// Lambda execution context public ActionGroupInvocationOutput Resolve(ActionGroupInvocationInput input, ILambdaContext? context = null) { return ResolveAsync(input, context).GetAwaiter().GetResult(); @@ -61,9 +257,8 @@ public ActionGroupInvocationOutput Resolve(ActionGroupInvocationInput input, ILa /// /// Asynchronously resolves and processes a Bedrock Agent function invocation. /// - /// The function invocation input - /// Lambda execution context - public async Task ResolveAsync(ActionGroupInvocationInput input, ILambdaContext? context = null) + public async Task ResolveAsync(ActionGroupInvocationInput input, + ILambdaContext? context = null) { return await Task.FromResult(HandleEvent(input, context)); } @@ -100,5 +295,53 @@ private ActionGroupInvocationOutput HandleEvent(ActionGroupInvocationInput input Text = $"No handler registered for function: {input.Function}" }; } + + private ActionGroupInvocationOutput ConvertToOutput(T result) + { + if (result == null) + { + return new ActionGroupInvocationOutput { Text = string.Empty }; + } + + // If result is already an ActionGroupInvocationOutput, return it directly + if (result is ActionGroupInvocationOutput output) + { + return output; + } + + // For primitive types and strings, convert to string + if (result is string str) + { + return new ActionGroupInvocationOutput { Text = str }; + } + + if (result is int intVal) + { + return new ActionGroupInvocationOutput { Text = intVal.ToString(CultureInfo.InvariantCulture) }; + } + + if (result is double doubleVal) + { + return new ActionGroupInvocationOutput { Text = doubleVal.ToString(CultureInfo.InvariantCulture) }; + } + + if (result is bool boolVal) + { + return new ActionGroupInvocationOutput { Text = boolVal.ToString() }; + } + + if (result is long longVal) + { + return new ActionGroupInvocationOutput { Text = longVal.ToString(CultureInfo.InvariantCulture) }; + } + + if (result is decimal decimalVal) + { + return new ActionGroupInvocationOutput { Text = decimalVal.ToString(CultureInfo.InvariantCulture) }; + } + + // For any other type, use ToString() instead of JSON serialization + return new ActionGroupInvocationOutput { Text = result.ToString() ?? string.Empty }; + } } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs new file mode 100644 index 00000000..003f86d2 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace AWS.Lambda.Powertools.EventHandler +{ + // Service provider-aware resolver + public class DIBedrockAgentFunctionResolver : BedrockAgentFunctionResolver + { + public IServiceProvider ServiceProvider { get; } + + public DIBedrockAgentFunctionResolver(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + } + + public static class BedrockResolverExtensions + { + // Extension to register the resolver in DI + public static IServiceCollection AddBedrockResolver(this IServiceCollection services) + { + services.AddSingleton(sp => + new DIBedrockAgentFunctionResolver(sp)); + return services; + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/ParameterAccessor.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/ParameterAccessor.cs new file mode 100644 index 00000000..51217357 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/ParameterAccessor.cs @@ -0,0 +1,126 @@ +using System.Globalization; +using Amazon.BedrockAgentRuntime.Model; + +namespace AWS.Lambda.Powertools.EventHandler; + +/// +/// Provides strongly-typed access to the parameters of an agent function call. +/// +public class ParameterAccessor +{ + private readonly List _parameters; + + internal ParameterAccessor(List? parameters) + { + _parameters = parameters ?? new List(); + } + + /// + /// Gets a parameter value by name with type conversion. + /// + public T Get(string name) + { + var parameter = _parameters.FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)); + if (parameter == null || parameter.Value == null) + { + return default!; + } + + return ConvertParameter(parameter); + } + + /// + /// Gets a parameter value by index with type conversion. + /// + public T GetAt(int index) + { + if (index < 0 || index >= _parameters.Count) + { + return default!; + } + + var parameter = _parameters[index]; + if (parameter.Value == null) + { + return default!; + } + + return ConvertParameter(parameter); + } + + /// + /// Gets a parameter value by name with fallback to a default value. + /// + public T GetOrDefault(string name, T defaultValue) + { + var parameter = _parameters.FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)); + if (parameter == null || parameter.Value == null) + { + return defaultValue; + } + + return ConvertParameter(parameter); + } + + private static T ConvertParameter(Parameter? parameter) + { + if (parameter == null || parameter.Value == null) + { + return default!; + } + + // Handle different types explicitly for AOT compatibility + if (typeof(T) == typeof(string)) + { + return (T)(object)parameter.Value; + } + + if (typeof(T) == typeof(int) || typeof(T) == typeof(int?)) + { + if (int.TryParse(parameter.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out int result)) + { + return (T)(object)result; + } + return default!; + } + + if (typeof(T) == typeof(double) || typeof(T) == typeof(double?)) + { + if (double.TryParse(parameter.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out double result)) + { + return (T)(object)result; + } + return default!; + } + + if (typeof(T) == typeof(bool) || typeof(T) == typeof(bool?)) + { + if (bool.TryParse(parameter.Value, out bool result)) + { + return (T)(object)result; + } + return default!; + } + + if (typeof(T) == typeof(long) || typeof(T) == typeof(long?)) + { + if (long.TryParse(parameter.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out long result)) + { + return (T)(object)result; + } + return default!; + } + + if (typeof(T) == typeof(decimal) || typeof(T) == typeof(decimal?)) + { + if (decimal.TryParse(parameter.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal result)) + { + return (T)(object)result; + } + return default!; + } + + // Return default for array and complex types + return default!; + } +} \ No newline at end of file diff --git a/libraries/src/Directory.Packages.props b/libraries/src/Directory.Packages.props index f9a4730c..a4421f6f 100644 --- a/libraries/src/Directory.Packages.props +++ b/libraries/src/Directory.Packages.props @@ -14,6 +14,7 @@ + diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs index a406a15f..e30dcd9b 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs @@ -1,7 +1,9 @@ using System.Text.Json; using System.Text.Json.Serialization; using Amazon.BedrockAgentRuntime.Model; +using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; +using Microsoft.Extensions.DependencyInjection; namespace AWS.Lambda.Powertools.EventHandler.Tests; @@ -100,7 +102,7 @@ public void TestFunctionHandlerWithInput() // Arrange var resolver = new BedrockAgentFunctionResolver(); resolver.Tool("TestFunction", - (input) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); + (ActionGroupInvocationInput input, ILambdaContext context) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); var input = new ActionGroupInvocationInput { Function = "TestFunction" }; var context = new TestLambdaContext(); @@ -118,7 +120,7 @@ public async Task TestFunctionHandlerWithInputAsync() // Arrange var resolver = new BedrockAgentFunctionResolver(); resolver.Tool("TestFunction", - (input) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); + (ActionGroupInvocationInput input) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); var input = new ActionGroupInvocationInput { Function = "TestFunction" }; var context = new TestLambdaContext(); @@ -208,23 +210,173 @@ public void TestFunctionHandlerWithEvent() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("sum_numbers", (payload, context ) => + resolver.Tool( + name: "GetCustomForecast", + description: "Get detailed forecast for a location", + handler: (string location, int days, ILambdaContext ctx) => { + ctx.Logger.LogLine($"Getting forecast for {location}"); + return $"{days}-day forecast for {location}"; + } + ); + + resolver.Tool( + name: "Greet", + description: "Greet a user", + handler: (string name) => { + return $"Hello {name}"; + } + ); + + resolver.Tool( + name: "Simple", + description: "Greet a user", + handler: () => { + return "Hello"; + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "GetCustomForecast", + Parameters = new List + { + new Parameter + { + Name = "location", + Value = "Lisbon", + Type = "String" + }, + new Parameter + { + Name = "days", + Value = "1", + Type = "Number" + } + } + }; + + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + Assert.Equal("1-day forecast for Lisbon", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithEventAndServices() + { + // Arrange + + // Setup DI + var services = new ServiceCollection(); + services.AddSingleton(new HttpClient()); + services.AddBedrockResolver(); + + var serviceProvider = services.BuildServiceProvider(); + var resolver = serviceProvider.GetRequiredService(); + + resolver.Tool( + name: "GetCustomForecast", + description: "Get detailed forecast for a location", + handler: async (string location, int days, HttpClient client, ILambdaContext ctx) => + { + var resp = await client.GetStringAsync("https://api.open-meteo.com/v1/forecast?latitude=38.7167&longitude=-9.1333¤t=temperature_2m"); + return resp; + } + ); + + resolver.Tool( + name: "Greet", + description: "Greet a user", + handler: (string name) => { + return $"Hello {name}"; + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "GetCustomForecast", + Parameters = new List + { + new Parameter + { + Name = "location", + Value = "Lisbon", + Type = "String" + }, + new Parameter + { + Name = "days", + Value = "1", + Type = "Number" + } + } + }; + + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + Assert.Equal("1-day forecast for Lisbon", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithEventTypes() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "GetCustomForecast", + description: "Get detailed forecast for a location", + handler: (string location, int days, ILambdaContext ctx) => { + ctx.Logger.LogLine($"Getting forecast for {location}"); + return $"{days}-day forecast for {location}"; + } + ); + + resolver.Tool( + name: "Greet", + description: "Greet a user", + handler: (string name) => { + return $"Hello {name}"; + } + ); + + var input = new ActionGroupInvocationInput { - - return new ActionGroupInvocationOutput { Text = "2" }; - }); + Function = "GetCustomForecast", + Parameters = new List + { + new Parameter + { + Name = "location", + Value = "Lisbon", + Type = "String" + }, + new Parameter + { + Name = "days", + Value = "1", + Type = "Number" + } + } + }; var context = new TestLambdaContext(); // Act - var result = resolver.Resolve(_bedrockEvent, context); + var result = resolver.Resolve(input, context); // Assert - Assert.Equal("2", result.Text); + Assert.Equal("1-day forecast for Lisbon", result.Text); } } -// Types + // String // Number // Integer From f5a8f59e762a2f92c1cba87b518990388b526d46 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Sun, 25 May 2025 20:29:05 +0100 Subject: [PATCH 28/52] feat(BedrockAgentFunctionResolver): enhance tool registration with detailed XML documentation and support for various handler signatures --- .../BedrockAgentFunctionResolver.cs | 336 ++++++++++++++---- .../BedrockAgentFunctionResolverExtensions.cs | 28 +- .../AppSyncEventsTests.cs | 69 ++-- .../BedrockAgentFunctionResolverTests.cs | 331 +++++++++++++++-- .../RouteHandlerRegistryTests.cs | 8 +- 5 files changed, 639 insertions(+), 133 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs index 5f33caa9..c78911b3 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs @@ -1,14 +1,26 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text.Json.Serialization; -using System.Threading.Tasks; +using System.Globalization; using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; +// ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler { + /// + /// A resolver for Bedrock Agent functions that allows registering handlers for tool functions. + /// + /// + /// Basic usage: + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool("GetWeather", (string city) => $"Weather in {city} is sunny"); + /// + /// // Lambda handler + /// public ActionGroupInvocationOutput FunctionHandler(ActionGroupInvocationInput input, ILambdaContext context) + /// { + /// return resolver.Resolve(input, context); + /// } + /// + /// public class BedrockAgentFunctionResolver { private readonly @@ -33,6 +45,23 @@ private static bool IsBedrockParameter(Type type) => /// /// Registers a handler that directly accepts ActionGroupInvocationInput and returns ActionGroupInvocationOutput /// + /// The name of the tool function + /// The handler function that accepts input and context and returns output + /// Optional description of the tool function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool( + /// "GetWeatherDetails", + /// (ActionGroupInvocationInput input, ILambdaContext context) => { + /// context.Logger.LogLine($"Processing request for {input.Function}"); + /// return new ActionGroupInvocationOutput { Text = "Weather details response" }; + /// }, + /// "Gets detailed weather information" + /// ); + /// + /// public BedrockAgentFunctionResolver Tool( string name, Func handler, @@ -48,6 +77,23 @@ public BedrockAgentFunctionResolver Tool( /// /// Registers a handler that directly accepts ActionGroupInvocationInput and returns ActionGroupInvocationOutput /// + /// The name of the tool function + /// The handler function that accepts input and returns output + /// Optional description of the tool function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool( + /// "GetWeatherDetails", + /// (ActionGroupInvocationInput input) => { + /// var city = input.Parameters.FirstOrDefault(p => p.Name == "city")?.Value; + /// return new ActionGroupInvocationOutput { Text = $"Weather in {city} is sunny" }; + /// }, + /// "Gets weather for a city" + /// ); + /// + /// public BedrockAgentFunctionResolver Tool( string name, Func handler, @@ -61,24 +107,199 @@ public BedrockAgentFunctionResolver Tool( } /// - /// Registers a handler for a tool function with automatically converted return type. + /// Registers a parameter-less handler that returns ActionGroupInvocationOutput /// + /// The name of the tool function + /// The handler function that returns output + /// Optional description of the tool function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool( + /// "GetCurrentTime", + /// () => new ActionGroupInvocationOutput { Text = DateTime.Now.ToString() }, + /// "Gets the current server time" + /// ); + /// + /// public BedrockAgentFunctionResolver Tool( string name, - string description = "", - Delegate? handler = null) + Func handler, + string description = "") + { + ArgumentNullException.ThrowIfNull(handler); + + _handlers[name] = (input, context) => handler(); + return this; + } + + /// + /// Registers a parameter-less handler with automatic string conversion + /// + /// The name of the tool function + /// The handler function that returns a string + /// Optional description of the tool function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool( + /// "GetGreeting", + /// () => "Hello, world!", + /// "Returns a greeting message" + /// ); + /// + /// + public BedrockAgentFunctionResolver Tool( + string name, + Func handler, + string description = "") + { + ArgumentNullException.ThrowIfNull(handler); + + _handlers[name] = (input, context) => new ActionGroupInvocationOutput { Text = handler() }; + return this; + } + + /// + /// Registers a parameter-less handler with automatic object conversion + /// + /// The name of the tool function + /// The handler function that returns an object + /// Optional description of the tool function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool( + /// "GetServerStatus", + /// () => new { Status = "Online", Uptime = "99.9%" }, + /// "Returns the server status information" + /// ); + /// + /// + public BedrockAgentFunctionResolver Tool( + string name, + Func handler, + string description = "") + { + ArgumentNullException.ThrowIfNull(handler); + + _handlers[name] = (input, context) => + { + var result = handler(); + return ConvertToOutput(result); + }; + return this; + } + + /// + /// Registers a handler for a tool function with automatically converted return type (no description). + /// + /// The name of the tool function + /// The delegate handler function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool( + /// "CalculateSum", + /// (int a, int b) => a + b + /// ); + /// + /// + public BedrockAgentFunctionResolver Tool( + string name, + Delegate handler) + { + return Tool(name, "", handler); + } + + /// + /// Registers a handler for a tool function with description and automatically converted return type. + /// + /// The name of the tool function + /// Description of the tool function + /// The delegate handler function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool( + /// "GetWeather", + /// "Gets the weather forecast for a specific city", + /// (string city, int days) => $"{days}-day forecast for {city}: Sunny" + /// ); + /// + /// + public BedrockAgentFunctionResolver Tool( + string name, + string description, + Delegate handler) { - // Delegate to the generic version with object as return type return Tool(name, description, handler); } /// - /// Registers a handler for a tool function with typed return value. + /// Registers a handler for a tool function with typed return value (no description). + /// + /// The return type of the handler + /// The name of the tool function + /// The delegate handler function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.Tool<int>( + /// "CalculateArea", + /// (int width, int height) => width * height + /// ); + /// + /// + public BedrockAgentFunctionResolver Tool( + string name, + Delegate handler) + { + return Tool(name, "", handler); + } + + /// + /// Registers a handler for a tool function with description and typed return value. /// + /// The return type of the handler + /// The name of the tool function + /// Description of the tool function + /// The delegate handler function + /// The resolver instance for method chaining + /// + /// + /// var resolver = new BedrockAgentFunctionResolver(); + /// + /// // Register a function with strongly typed parameters and return value + /// resolver.Tool<double>( + /// "CalculateDistance", + /// "Calculates the distance between two points", + /// (double x1, double y1, double x2, double y2) => { + /// return Math.Sqrt(Math.Pow(x2 - x1, 2) + Math.Pow(y2 - y1, 2)); + /// } + /// ); + /// + /// // Register a function that accepts Lambda context + /// resolver.Tool<string>( + /// "LogAndReturn", + /// "Logs a message and returns it", + /// (string message, ILambdaContext context) => { + /// context.Logger.LogLine($"Message received: {message}"); + /// return message; + /// } + /// ); + /// + /// public BedrockAgentFunctionResolver Tool( string name, - string description = "", - Delegate? handler = null) + string description, + Delegate handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); @@ -91,7 +312,7 @@ public BedrockAgentFunctionResolver Tool( var bedrockParamIndex = 0; // Get service provider from resolver if available - var serviceProvider = (this as DIBedrockAgentFunctionResolver)?.ServiceProvider; + var serviceProvider = (this as DiBedrockAgentFunctionResolver)?.ServiceProvider; // Map parameters from Bedrock input and DI for (var i = 0; i < parameters.Length; i++) @@ -197,58 +418,25 @@ public BedrockAgentFunctionResolver Tool( return this; } - /// - /// Registers a parameter-less handler that returns ActionGroupInvocationOutput - /// - public BedrockAgentFunctionResolver Tool( - string name, - Func handler, - string description = "") - { - if (handler == null) - throw new ArgumentNullException(nameof(handler)); - - _handlers[name] = (input, context) => handler(); - return this; - } - - /// - /// Registers a parameter-less handler with automatic string conversion - /// - public BedrockAgentFunctionResolver Tool( - string name, - Func handler, - string description = "") - { - if (handler == null) - throw new ArgumentNullException(nameof(handler)); - - _handlers[name] = (input, context) => new ActionGroupInvocationOutput { Text = handler() }; - return this; - } - - /// - /// Registers a parameter-less handler with automatic object conversion - /// - public BedrockAgentFunctionResolver Tool( - string name, - Func handler, - string description = "") - { - if (handler == null) - throw new ArgumentNullException(nameof(handler)); - - _handlers[name] = (input, context) => - { - var result = handler(); - return ConvertToOutput(result); - }; - return this; - } - /// /// Resolves and processes a Bedrock Agent function invocation. /// + /// The Bedrock Agent input containing the function name and parameters + /// Optional Lambda context + /// The output from the function execution + /// + /// + /// // Lambda handler + /// public ActionGroupInvocationOutput FunctionHandler(ActionGroupInvocationInput input, ILambdaContext context) + /// { + /// var resolver = new BedrockAgentFunctionResolver() + /// .Tool("GetWeather", (string city) => $"Weather in {city} is sunny") + /// .Tool("GetTime", () => DateTime.Now.ToString()); + /// + /// return resolver.Resolve(input, context); + /// } + /// + /// public ActionGroupInvocationOutput Resolve(ActionGroupInvocationInput input, ILambdaContext? context = null) { return ResolveAsync(input, context).GetAwaiter().GetResult(); @@ -257,6 +445,26 @@ public ActionGroupInvocationOutput Resolve(ActionGroupInvocationInput input, ILa /// /// Asynchronously resolves and processes a Bedrock Agent function invocation. /// + /// The Bedrock Agent input containing the function name and parameters + /// Optional Lambda context + /// A task that completes with the output from the function execution + /// + /// + /// // Async Lambda handler + /// public async Task<ActionGroupInvocationOutput> FunctionHandler(ActionGroupInvocationInput input, ILambdaContext context) + /// { + /// var resolver = new BedrockAgentFunctionResolver() + /// .Tool("GetWeatherAsync", async (string city) => { + /// // Simulate API call + /// await Task.Delay(100); + /// return $"Weather in {city} is sunny"; + /// }) + /// .Tool("GetTime", () => DateTime.Now.ToString()); + /// + /// return await resolver.ResolveAsync(input, context); + /// } + /// + /// public async Task ResolveAsync(ActionGroupInvocationInput input, ILambdaContext? context = null) { @@ -344,4 +552,4 @@ private ActionGroupInvocationOutput ConvertToOutput(T result) return new ActionGroupInvocationOutput { Text = result.ToString() ?? string.Empty }; } } -} \ No newline at end of file +} diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs index 003f86d2..871e01b3 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs @@ -1,26 +1,42 @@ -using System; using Microsoft.Extensions.DependencyInjection; +// ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler { - // Service provider-aware resolver - public class DIBedrockAgentFunctionResolver : BedrockAgentFunctionResolver + /// + /// Extended Bedrock Agent Function Resolver with dependency injection support. + /// + public class DiBedrockAgentFunctionResolver : BedrockAgentFunctionResolver { + /// + /// Gets the service provider used for dependency injection. + /// public IServiceProvider ServiceProvider { get; } - public DIBedrockAgentFunctionResolver(IServiceProvider serviceProvider) + /// + /// Initializes a new instance of the class. + /// + /// The service provider for dependency injection. + public DiBedrockAgentFunctionResolver(IServiceProvider serviceProvider) { ServiceProvider = serviceProvider; } } + /// + /// Extension methods for Bedrock Agent Function Resolver. + /// public static class BedrockResolverExtensions { - // Extension to register the resolver in DI + /// + /// Registers a Bedrock Agent Function Resolver with dependency injection support. + /// + /// The service collection to add the resolver to. + /// The updated service collection. public static IServiceCollection AddBedrockResolver(this IServiceCollection services) { services.AddSingleton(sp => - new DIBedrockAgentFunctionResolver(sp)); + new DiBedrockAgentFunctionResolver(sp)); return services; } } diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AppSyncEventsTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AppSyncEventsTests.cs index b4301f93..0b8103f4 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AppSyncEventsTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AppSyncEventsTests.cs @@ -3,6 +3,8 @@ using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.EventHandler.AppSyncEvents; +#pragma warning disable CS8604 // Possible null reference argument. +#pragma warning disable CS8602 // Dereference of a possibly null reference. namespace AWS.Lambda.Powertools.EventHandler.Tests; @@ -74,10 +76,10 @@ public async Task Should_Return_Unchanged_Payload_Async() var lambdaContext = new TestLambdaContext(); var app = new AppSyncEventsResolver(); - app.OnPublishAsync("/default/channel", async payload => + app.OnPublishAsync("/default/channel", payload => { // Handle channel1 events - return payload; + return Task.FromResult(payload); }); // Act @@ -101,7 +103,7 @@ public async Task Should_Handle_Error_In_Event_Processing() var lambdaContext = new TestLambdaContext(); var app = new AppSyncEventsResolver(); - app.OnPublishAsync("/default/channel", async (payload) => + app.OnPublishAsync("/default/channel", (payload) => { // Throw exception for second event if (payload.ContainsKey("event_2")) @@ -109,7 +111,7 @@ public async Task Should_Handle_Error_In_Event_Processing() throw new InvalidOperationException("Test error"); } - return payload; + return Task.FromResult(payload); }); // Act @@ -137,10 +139,10 @@ public async Task Should_Match_Path_With_Wildcard() var app = new AppSyncEventsResolver(); int callCount = 0; - app.OnPublishAsync("/default/*", async (payload) => + app.OnPublishAsync("/default/*", (payload) => { callCount++; - return new Dictionary { ["wildcard_matched"] = true }; + return Task.FromResult(new Dictionary { ["wildcard_matched"] = true }); }); // Act @@ -162,9 +164,9 @@ public async Task Should_Authorize_Subscription() var lambdaContext = new TestLambdaContext(); var app = new AppSyncEventsResolver(); - app.OnPublishAsync("/default/channel", async (payload) => payload); + app.OnPublishAsync("/default/channel", (payload) => Task.FromResult(payload)); - app.OnSubscribeAsync("/default/*", async (info) => true); + app.OnSubscribeAsync("/default/*", (info) => Task.FromResult(true)); var subscribeEvent = new AppSyncEventsRequest { Info = new Information @@ -263,8 +265,7 @@ public async Task Should_Handle_Error_In_Aggregate_Mode_Async() var lambdaContext = new TestLambdaContext(); var app = new AppSyncEventsResolver(); - app.OnPublishAggregateAsync("/default/channel", - async (evt, ctx) => { throw new InvalidOperationException("Aggregate error"); }); + app.OnPublishAggregateAsync("/default/channel", (evt, ctx) => { throw new InvalidOperationException("Aggregate error"); }); // Act var result = await app.ResolveAsync(_appSyncEvent, lambdaContext); @@ -311,7 +312,7 @@ public async Task Should_Handle_TransformingPayload_Async() var lambdaContext = new TestLambdaContext(); var app = new AppSyncEventsResolver(); - app.OnPublishAsync("/default/channel", async (payload) => + app.OnPublishAsync("/default/channel", (payload) => { // Transform each event payload var transformedPayload = new Dictionary(); @@ -320,7 +321,7 @@ public async Task Should_Handle_TransformingPayload_Async() transformedPayload[$"transformed_{key}"] = $"transformed_{payload[key]}"; } - return transformedPayload; + return Task.FromResult(transformedPayload); }); // Act @@ -462,11 +463,9 @@ public async Task Should_Replace_Handler_When_RegisteringTwice_Async() var lambdaContext = new TestLambdaContext(); var app = new AppSyncEventsResolver(); - app.OnPublishAsync("/default/channel", - async (payload) => { return new Dictionary { ["handler"] = "first" }; }); + app.OnPublishAsync("/default/channel", (payload) => { return Task.FromResult(new Dictionary { ["handler"] = "first" }); }); - app.OnPublishAsync("/default/channel", - async (payload) => { return new Dictionary { ["handler"] = "second" }; }); + app.OnPublishAsync("/default/channel", (payload) => { return Task.FromResult(new Dictionary { ["handler"] = "second" }); }); // Act var result = await app.ResolveAsync(_appSyncEvent, lambdaContext); @@ -513,7 +512,7 @@ public async Task Aggregate_Handler_Can_Return_Individual_Results_With_Ids() app.OnPublishAsync("/default/channel12", (payload) => { throw new Exception("My custom exception"); }); - app.OnPublishAggregateAsync("/default/channel", async (evt) => + app.OnPublishAggregateAsync("/default/channel", (evt) => { // Iterate through events and return individual results with IDs var results = new List(); @@ -555,7 +554,7 @@ public async Task Aggregate_Handler_Can_Return_Individual_Results_With_Ids() } } - return new AppSyncEventsResponse { Events = results }; + return Task.FromResult(new AppSyncEventsResponse { Events = results }); }); // Act @@ -583,13 +582,13 @@ public async Task Should_Verify_Ids_Are_Preserved_In_Error_Case() var app = new AppSyncEventsResolver(); // Create handlers that throw exceptions for specific events - app.OnPublishAsync("/default/channel", async (payload) => + app.OnPublishAsync("/default/channel", (payload) => { if (payload.ContainsKey("event_1")) throw new InvalidOperationException("Error for event 1"); if (payload.ContainsKey("event_3")) throw new ArgumentException("Error for event 3"); - return payload; + return Task.FromResult(payload); }); // Act @@ -615,16 +614,16 @@ public async Task Should_Match_Most_Specific_Handler_Only() int firstHandlerCalls = 0; int secondHandlerCalls = 0; - app.OnPublishAsync("/default/channel", async (payload) => + app.OnPublishAsync("/default/channel", (payload) => { firstHandlerCalls++; - return new Dictionary { ["handler"] = "first" }; + return Task.FromResult(new Dictionary { ["handler"] = "first" }); }); - app.OnPublishAsync("/default/*", async (payload) => + app.OnPublishAsync("/default/*", (payload) => { secondHandlerCalls++; - return new Dictionary { ["handler"] = "second" }; + return Task.FromResult(new Dictionary { ["handler"] = "second" }); }); // Act @@ -667,18 +666,18 @@ public async Task Should_Handle_Multiple_Keys_In_Payload() ] }; - app.OnPublishAsync("/default/channel", async (payload) => + app.OnPublishAsync("/default/channel", (payload) => { // Check that both keys are present Assert.Equal("data_1", payload["event_1"]); Assert.Equal("data_1a", payload["event_1a"]); // Return a processed result with both keys - return new Dictionary + return Task.FromResult(new Dictionary { ["processed_1"] = payload["event_1"], ["processed_1a"] = payload["event_1a"] - }; + }); }); // Act @@ -699,14 +698,11 @@ public async Task Should_Only_Use_First_Matching_Handler_By_Specificity() var app = new AppSyncEventsResolver(); // Register handlers with different specificity - app.OnPublishAsync("/*", async (payload) => - new Dictionary { ["handler"] = "least-specific" }); + app.OnPublishAsync("/*", (payload) => Task.FromResult(new Dictionary { ["handler"] = "least-specific" })); - app.OnPublishAsync("/default/*", async (payload) => - new Dictionary { ["handler"] = "more-specific" }); + app.OnPublishAsync("/default/*", (payload) => Task.FromResult(new Dictionary { ["handler"] = "more-specific" })); - app.OnPublishAsync("/default/channel", async (payload) => - new Dictionary { ["handler"] = "most-specific" }); + app.OnPublishAsync("/default/channel", (payload) => Task.FromResult(new Dictionary { ["handler"] = "most-specific" })); // Act var result = await app.ResolveAsync(_appSyncEvent, lambdaContext); @@ -744,8 +740,7 @@ public async Task Should_Fallback_To_Less_Specific_Handler_If_No_Exact_Match() ] }; - app.OnPublishAsync("/default/*", async (payload) => - new Dictionary { ["handler"] = "wildcard-handler" }); + app.OnPublishAsync("/default/*", (payload) => Task.FromResult(new Dictionary { ["handler"] = "wildcard-handler" })); // Act var result = await app.ResolveAsync(fallbackEvent, lambdaContext); @@ -763,7 +758,7 @@ public async Task Should_Return_Null_When_Subscribing_To_Path_Without_Publish_Ha var app = new AppSyncEventsResolver(); // Only set up a subscribe handler without corresponding publish handler - app.OnSubscribeAsync("/subscribe-only", async (info) => true); + app.OnSubscribeAsync("/subscribe-only", (info) => Task.FromResult(true)); var subscribeEvent = new AppSyncEventsRequest { @@ -824,7 +819,7 @@ public async Task Should_Return_UnauthorizedException_When_Throwing_Unauthorized var lambdaContext = new TestLambdaContext(); var app = new AppSyncEventsResolver(); - app.OnPublishAsync(publishPath, async (payload) => payload); + app.OnPublishAsync(publishPath, (payload) => Task.FromResult(payload)); app.OnSubscribeAsync(subscribePath, (info, lambdaContext) => { throw new UnauthorizedException("OOPS"); }); diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs index e30dcd9b..4e3ba51c 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs @@ -1,9 +1,11 @@ +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; using Microsoft.Extensions.DependencyInjection; +#pragma warning disable CS0162 // Unreachable code detected namespace AWS.Lambda.Powertools.EventHandler.Tests; @@ -102,7 +104,7 @@ public void TestFunctionHandlerWithInput() // Arrange var resolver = new BedrockAgentFunctionResolver(); resolver.Tool("TestFunction", - (ActionGroupInvocationInput input, ILambdaContext context) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); + (input, context) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); var input = new ActionGroupInvocationInput { Function = "TestFunction" }; var context = new TestLambdaContext(); @@ -120,7 +122,7 @@ public async Task TestFunctionHandlerWithInputAsync() // Arrange var resolver = new BedrockAgentFunctionResolver(); resolver.Tool("TestFunction", - (ActionGroupInvocationInput input) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); + input => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); var input = new ActionGroupInvocationInput { Function = "TestFunction" }; var context = new TestLambdaContext(); @@ -176,10 +178,6 @@ public void TestFunctionHandlerWithParameters() var input = new ActionGroupInvocationInput { Function = "TestFunction", - RequestBody = new RequestBody - { - - }, Parameters = new List { new Parameter @@ -267,11 +265,9 @@ public void TestFunctionHandlerWithEvent() [Fact] public void TestFunctionHandlerWithEventAndServices() { - // Arrange - // Setup DI var services = new ServiceCollection(); - services.AddSingleton(new HttpClient()); + services.AddSingleton(new MyImplementation()); services.AddBedrockResolver(); var serviceProvider = services.BuildServiceProvider(); @@ -280,21 +276,13 @@ public void TestFunctionHandlerWithEventAndServices() resolver.Tool( name: "GetCustomForecast", description: "Get detailed forecast for a location", - handler: async (string location, int days, HttpClient client, ILambdaContext ctx) => + handler: async (string location, int days, IMyInterface client, ILambdaContext ctx) => { - var resp = await client.GetStringAsync("https://api.open-meteo.com/v1/forecast?latitude=38.7167&longitude=-9.1333¤t=temperature_2m"); + var resp = await client.DoSomething(location , days); return resp; } ); - resolver.Tool( - name: "Greet", - description: "Greet a user", - handler: (string name) => { - return $"Hello {name}"; - } - ); - var input = new ActionGroupInvocationInput { Function = "GetCustomForecast", @@ -321,7 +309,7 @@ public void TestFunctionHandlerWithEventAndServices() var result = resolver.Resolve(input, context); // Assert - Assert.Equal("1-day forecast for Lisbon", result.Text); + Assert.Equal("Forecast for Lisbon for 1 days", result.Text); } [Fact] @@ -374,11 +362,304 @@ public void TestFunctionHandlerWithEventTypes() // Assert Assert.Equal("1-day forecast for Lisbon", result.Text); } + + [Fact] + public void TestFunctionHandlerWithBooleanParameter() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "TestBool", + description: "Test boolean parameter", + handler: (bool isEnabled) => { + return $"Feature is {(isEnabled ? "enabled" : "disabled")}"; + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "TestBool", + Parameters = new List + { + new Parameter + { + Name = "isEnabled", + Value = "true", + Type = "Boolean" + } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Feature is enabled", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithMissingRequiredParameter() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "RequiredParam", + description: "Function with required parameter", + handler: (string name) => $"Hello, {name}!" + ); + + var input = new ActionGroupInvocationInput + { + Function = "RequiredParam", + Parameters = new List() // Empty parameters + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains("Hello, !", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithMultipleParameterTypes() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "ComplexFunction", + description: "Test multiple parameter types", + handler: (string name, int count, bool isActive) => { + return $"Name: {name}, Count: {count}, Active: {isActive}"; + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "ComplexFunction", + Parameters = new List + { + new Parameter { Name = "name", Value = "Test", Type = "String" }, + new Parameter { Name = "count", Value = "5", Type = "Integer" }, + new Parameter { Name = "isActive", Value = "true", Type = "Boolean" } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Name: Test, Count: 5, Active: True", result.Text); + } + + public enum TestEnum + { + Option1, + Option2, + Option3 + } + + [Fact] + public void TestFunctionHandlerWithEnumParameter() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "EnumTest", + description: "Test enum parameter", + handler: (TestEnum option) => { + return $"Selected option: {option}"; + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "EnumTest", + Parameters = new List + { + new Parameter + { + Name = "option", + Value = "Option2", + Type = "String" // Enums come as strings + } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Selected option: Option2", result.Text); + } + + [Fact] + public void TestParameterNameCaseSensitivity() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "CaseTest", + description: "Test case sensitivity", + handler: (string userName) => $"Hello, {userName}!" + ); + + var input = new ActionGroupInvocationInput + { + Function = "CaseTest", + Parameters = new List + { + new Parameter + { + Name = "UserName", // Different case than parameter + Value = "John", + Type = "String" + } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Hello, John!", result.Text); + } + + [Fact] + public void TestParameterOrderIndependence() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "OrderTest", + description: "Test parameter order independence", + handler: (string firstName, string lastName) => { + return $"Name: {firstName} {lastName}"; + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "OrderTest", + Parameters = new List + { + // Parameters in reverse order of handler parameters + new Parameter { Name = "lastName", Value = "Smith", Type = "String" }, + new Parameter { Name = "firstName", Value = "John", Type = "String" } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Name: John Smith", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithDecimalParameter() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "PriceCalculator", + description: "Calculate total price with tax", + handler: (decimal price) => { + var withTax = price * 1.2m; + return $"Total price with tax: {withTax.ToString("F2", CultureInfo.InvariantCulture)}"; + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "PriceCalculator", + Parameters = new List + { + new Parameter + { + Name = "price", + Value = "29.99", + Type = "Number" + } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains("35.99", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithArrayParameter() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "ArrayTest", + description: "Test with array parameter", + handler: (string text) => { + // In a real implementation, you'd parse the array from the string + // ActionGroupInvocationInput doesn't directly support array types + return $"Received: {text}"; + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "ArrayTest", + Parameters = new List + { + new Parameter + { + Name = "text", + Value = "[\"item1\",\"item2\"]", // Array as JSON string + Type = "Array" + } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Received: [\"item1\",\"item2\"]", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithExceptionInHandler() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "ThrowingFunction", + description: "Function that throws exception", + handler: () => { + throw new InvalidOperationException("Test error"); + return "This will not run:"; + } + ); + + var input = new ActionGroupInvocationInput { Function = "ThrowingFunction" }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains("Error executing function", result.Text); + } } +internal interface IMyInterface +{ + Task DoSomething(string location, int days); +} -// String -// Number -// Integer -// Boolean -// Array \ No newline at end of file +internal class MyImplementation : IMyInterface +{ + public async Task DoSomething(string location, int days) + { + return await Task.FromResult($"Forecast for {location} for {days} days"); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/RouteHandlerRegistryTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/RouteHandlerRegistryTests.cs index 92c9da3a..f0437dc9 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/RouteHandlerRegistryTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/RouteHandlerRegistryTests.cs @@ -1,7 +1,13 @@ +using System.Diagnostics.CodeAnalysis; using AWS.Lambda.Powertools.EventHandler.Internal; +#pragma warning disable CS8605 // Unboxing a possibly null value. +#pragma warning disable CS8601 // Possible null reference assignment. +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. +#pragma warning disable CS8602 // Dereference of a possibly null reference. namespace AWS.Lambda.Powertools.EventHandler.Tests; +[SuppressMessage("Usage", "xUnit1031:Do not use blocking task operations in test method")] public class RouteHandlerRegistryTests { [Theory] @@ -14,7 +20,7 @@ public class RouteHandlerRegistryTests [InlineData("default/*", false)] // Not starting with slash [InlineData("", false)] // Empty path [InlineData(null, false)] // Null path - public void IsValidPath_ShouldValidateCorrectly(string path, bool expected) + public void IsValidPath_ShouldValidateCorrectly(string? path, bool expected) { // Create a private method accessor to test private IsValidPath method var registry = new RouteHandlerRegistry(); From ef41a10051cce40be1c48870127487f2bf69e2af Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 26 May 2025 12:40:13 +0100 Subject: [PATCH 29/52] add array parsing and documentation --- .../event_handler/bedrock_agent_function.md | 307 ++++++++++++++++++ .../BedrockAgentFunctionResolver.cs | 58 +++- .../Readme.md | 307 ++++++++++++++++-- .../BedrockAgentFunctionResolverTests.cs | 204 +++++++++--- mkdocs.yml | 1 + 5 files changed, 793 insertions(+), 84 deletions(-) create mode 100644 docs/core/event_handler/bedrock_agent_function.md diff --git a/docs/core/event_handler/bedrock_agent_function.md b/docs/core/event_handler/bedrock_agent_function.md new file mode 100644 index 00000000..0b2b866e --- /dev/null +++ b/docs/core/event_handler/bedrock_agent_function.md @@ -0,0 +1,307 @@ +--- +title: Bedrock Agent Function Resolver +description: Event Handler - Bedrock Agent Function Resolver +--- + +# 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. + +## Features + +- **Simple Tool Registration**: Register functions with descriptive names that Bedrock Agents can invoke +- **Automatic Parameter Handling**: Parameters are automatically extracted from Bedrock Agent requests and converted to the appropriate types +- **Type Safety**: Strongly typed parameters and return values +- **Multiple Return Types**: Support for returning strings, primitive types, objects, or custom types +- **Flexible Input Options**: Support for various parameter types including string, int, bool, DateTime, and enums +- **Lambda Context Access**: Easy access to Lambda context for logging and AWS Lambda features +- **Dependency Injection Support**: Seamless integration with .NET's dependency injection system + +## Installation + +Install the package via NuGet: + +```bash +dotnet add package AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver +``` + +## Basic Usage + +Here's a simple example showing how to register and use tool functions: + +```csharp +using Amazon.BedrockAgentRuntime.Model; +using Amazon.Lambda.Core; +using AWS.Lambda.Powertools.EventHandler; + +[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 ActionGroupInvocationOutput FunctionHandler( + ActionGroupInvocationInput 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. + +## Advanced Usage + +### Functions with Descriptions + +Add descriptive information to your tool functions: + +```csharp +_resolver.Tool( + "CheckInventory", + "Checks if a product is available in inventory", + (string productId, bool checkWarehouse) => + { + return checkWarehouse + ? $"Product {productId} has 15 units in warehouse" + : $"Product {productId} has 5 units in store"; + }); +``` + +### Accessing Lambda Context + +Access the Lambda context in your functions: + +```csharp +_resolver.Tool( + "LogRequest", + "Logs request information and returns confirmation", + (string requestId, ILambdaContext context) => + { + context.Logger.LogLine($"Processing request {requestId}"); + return $"Request {requestId} logged successfully"; + }); +``` + +### Working with Complex Return Types + +Return complex objects that will be converted to appropriate responses: + +```csharp +public class WeatherReport +{ + public string City { get; set; } + public string Conditions { get; set; } + public int Temperature { get; set; } + + public override string ToString() + { + return $"Weather in {City}: {Conditions}, {Temperature}°F"; + } +} + +_resolver.Tool( + "GetDetailedWeather", + "Returns detailed weather information for a location", + (string city) => new WeatherReport + { + City = city, + Conditions = "Partly Cloudy", + Temperature = 72 + }); +``` + +### Asynchronous Functions + +Register and use asynchronous functions: + +```csharp +_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: + +```csharp +_resolver.Tool( + "ProcessRawRequest", + "Processes the raw Bedrock Agent request", + (ActionGroupInvocationInput 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: + +```csharp +using Microsoft.Extensions.DependencyInjection; + +// Set up dependency injection +var services = new ServiceCollection(); +services.AddSingleton(); +services.AddBedrockResolver(); // Extension method to register the resolver + +var serviceProvider = services.BuildServiceProvider(); +var resolver = serviceProvider.GetRequiredService(); + +// 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); + }); +``` + +## 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: + - Finds the registered handler for the requested function + - Extracts and converts parameters to the correct types + - Invokes your handler with the parameters + - Formats the response in the way Bedrock Agents expect +5. The agent receives the response and uses it to continue the conversation with the user + +## Supported Parameter Types + +- `string` +- `int` / `long` +- `double` / `decimal` +- `bool` +- `DateTime` +- `Guid` +- `enum` types +- `ILambdaContext` (for accessing Lambda context) +- `ActionGroupInvocationInput` (for accessing raw request) +- Any service registered in dependency injection + +## Benefits + +- **Reduced Boilerplate**: Eliminate repetitive code for parsing requests and formatting responses +- **Type Safety**: Strong typing for parameters and return values +- **Simplified Development**: Focus on business logic instead of request/response handling +- **Reusable Components**: Build a library of tool functions that can be shared across agents +- **Easy Testing**: Functions can be easily unit tested in isolation +- **Flexible Integration**: Works seamlessly with AWS Lambda and Bedrock Agents + +## Complete Example with Dependency Injection + +```csharp +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(); + services.AddSingleton(); + services.AddBedrockResolver(); // Extension method to register the resolver + + var serviceProvider = services.BuildServiceProvider(); + _resolver = serviceProvider.GetRequiredService(); + + // 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); + } + } +} +``` + +## Learn More + +For more information about Amazon Bedrock Agents and function integration, see the [Amazon Bedrock documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-tools.html). diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs index c78911b3..bcf61e67 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs @@ -1,10 +1,22 @@ using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; // ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler { + [JsonSerializable(typeof(string[]))] + [JsonSerializable(typeof(int[]))] + [JsonSerializable(typeof(long[]))] + [JsonSerializable(typeof(double[]))] + [JsonSerializable(typeof(bool[]))] + [JsonSerializable(typeof(decimal[]))] + internal partial class BedrockFunctionResolverContext : JsonSerializerContext + { + } + /// /// A resolver for Bedrock Agent functions that allows registering handlers for tool functions. /// @@ -36,11 +48,18 @@ private readonly typeof(bool), typeof(decimal), typeof(DateTime), - typeof(Guid) + typeof(Guid), + typeof(string[]), + typeof(int[]), + typeof(long[]), + typeof(double[]), + typeof(bool[]), + typeof(decimal[]) }; private static bool IsBedrockParameter(Type type) => - _bedrockParameterTypes.Contains(type) || type.IsEnum; + _bedrockParameterTypes.Contains(type) || type.IsEnum || + (type.IsArray && _bedrockParameterTypes.Contains(type.GetElementType()!)); /// /// Registers a handler that directly accepts ActionGroupInvocationInput and returns ActionGroupInvocationOutput @@ -333,6 +352,41 @@ public BedrockAgentFunctionResolver Tool( var paramName = parameter.Name ?? $"arg{bedrockParamIndex}"; // AOT-compatible parameter access - direct type checks + // Array parameter handling + if (paramType.IsArray) + { + var jsonArrayStr = accessor.Get(paramName); + + if (!string.IsNullOrEmpty(jsonArrayStr)) + { + try + { + // AOT-compatible deserialization using source generation + if (paramType == typeof(string[])) + args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.StringArray); + else if (paramType == typeof(int[])) + args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.Int32Array); + else if (paramType == typeof(long[])) + args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.Int64Array); + else if (paramType == typeof(double[])) + args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.DoubleArray); + else if (paramType == typeof(bool[])) + args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.BooleanArray); + else if (paramType == typeof(decimal[])) + args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.DecimalArray); + else + args[i] = null; // Unsupported array type + } + catch (JsonException) + { + args[i] = null; + } + } + else + { + args[i] = null; + } + } if (paramType == typeof(string)) args[i] = accessor.Get(paramName); else if (paramType == typeof(int)) diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md index 08947c02..decd8abb 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md @@ -1,34 +1,38 @@ # AWS Lambda Powertools for .NET - Bedrock Agent Function Resolver ## Overview -The Bedrock Agent Function Resolver is a custom function resolver for AWS Lambda Powertools for .NET. It is designed to work with the Bedrock Agent, a tool that simplifies the process of building and deploying serverless applications on AWS Lambda. -The Bedrock Agent Function Resolver allows you to easily resolve and invoke Lambda functions using the Bedrock Agent's conventions and best practices. -This custom function resolver is part of the AWS Lambda Powertools for .NET library, which provides a suite of utilities for building serverless applications on AWS Lambda. +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. + ## Features -- Custom function resolver for AWS Lambda Powertools for .NET -- Supports Bedrock Agent conventions and best practices -- Simplifies the process of resolving and invoking Lambda functions -- Integrates with AWS Lambda Powertools for .NET library -- Supports dependency injection and configuration -- Provides a consistent and easy-to-use API for resolving functions -- Supports asynchronous and synchronous function invocation -- Supports error handling and logging -- Supports custom serialization and deserialization -- Supports custom middleware and filters - -## Getting Started -To get started with the Bedrock Agent Function Resolver, you need to install the AWS Lambda Powertools for .NET library and the Bedrock Agent Function Resolver package. You can do this using NuGet: + +- **Simple Tool Registration**: Register functions with descriptive names that Bedrock Agents can invoke +- **Automatic Parameter Handling**: Parameters are automatically extracted from Bedrock Agent requests and converted to the appropriate types +- **Type Safety**: Strongly typed parameters and return values +- **Multiple Return Types**: Support for returning strings, primitive types, objects, or custom types +- **Flexible Input Options**: Support for various parameter types including string, int, bool, DateTime, and enums +- **Lambda Context Access**: Easy access to Lambda context for logging and AWS Lambda features +- **Dependency Injection Support**: Seamless integration with .NET's dependency injection system +- **Error Handling**: Automatic error capturing and formatting for responses +- **Async Support**: First-class support for asynchronous function execution + +## Installation + +Install the package via NuGet: ```bash dotnet add package AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver ``` -## Usage -To use the Bedrock Agent Function Resolver, you need to create an instance of the `BedrockAgentFunctionResolver` class and register it with the AWS Lambda Powertools for .NET library. You can do this in your Lambda function's entry point: + +## Basic Usage + +Here's a simple example showing how to register and use tool functions: ```csharp +using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; -using Amazon.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver; - +using AWS.Lambda.Powertools.EventHandler; [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] @@ -36,22 +40,265 @@ namespace MyLambdaFunction { public class Function { - private readonly BedrockAgentFunctionResolver _functionResolver; - + private readonly BedrockAgentFunctionResolver _resolver; + public Function() { - // Create an instance of the Bedrock Agent Function Resolver - _functionResolver = new BedrockAgentFunctionResolver(); + _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 ActionGroupInvocationOutput FunctionHandler( + ActionGroupInvocationInput 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. + +## Advanced Usage + +### Functions with Descriptions + +Add descriptive information to your tool functions: + +```csharp +_resolver.Tool( + "CheckInventory", + "Checks if a product is available in inventory", + (string productId, bool checkWarehouse) => + { + return checkWarehouse + ? $"Product {productId} has 15 units in warehouse" + : $"Product {productId} has 5 units in store"; + }); +``` + +### Accessing Lambda Context - public async Task FunctionHandler(ILambdaContext context) +Access the Lambda context in your functions: + +```csharp +_resolver.Tool( + "LogRequest", + "Logs request information and returns confirmation", + (string requestId, ILambdaContext context) => + { + context.Logger.LogLine($"Processing request {requestId}"); + return $"Request {requestId} logged successfully"; + }); +``` + +### Working with Complex Return Types + +Return complex objects that will be converted to appropriate responses: + +```csharp +public class WeatherReport +{ + public string City { get; set; } + public string Conditions { get; set; } + public int Temperature { get; set; } + + public override string ToString() + { + return $"Weather in {City}: {Conditions}, {Temperature}°F"; + } +} + +_resolver.Tool( + "GetDetailedWeather", + "Returns detailed weather information for a location", + (string city) => new WeatherReport + { + City = city, + Conditions = "Partly Cloudy", + Temperature = 72 + }); +``` + +### Asynchronous Functions + +Register and use asynchronous functions: + +```csharp +_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: + +```csharp +_resolver.Tool( + "ProcessRawRequest", + "Processes the raw Bedrock Agent request", + (ActionGroupInvocationInput 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: + +```csharp +using Microsoft.Extensions.DependencyInjection; + +// Set up dependency injection +var services = new ServiceCollection(); +services.AddSingleton(); +services.AddBedrockResolver(); // Extension method to register the resolver + +var serviceProvider = services.BuildServiceProvider(); +var resolver = serviceProvider.GetRequiredService(); + +// 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); + }); +``` + +## 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: + - Finds the registered handler for the requested function + - Extracts and converts parameters to the correct types + - Invokes your handler with the parameters + - Formats the response in the way Bedrock Agents expect +5. The agent receives the response and uses it to continue the conversation with the user + +## Supported Parameter Types + +- `string` +- `int` / `long` +- `double` / `decimal` +- `bool` +- `DateTime` +- `Guid` +- `enum` types +- `ILambdaContext` (for accessing Lambda context) +- `ActionGroupInvocationInput` (for accessing raw request) +- Any service registered in dependency injection + +## Benefits + +- **Reduced Boilerplate**: Eliminate repetitive code for parsing requests and formatting responses +- **Type Safety**: Strong typing for parameters and return values +- **Simplified Development**: Focus on business logic instead of request/response handling +- **Reusable Components**: Build a library of tool functions that can be shared across agents +- **Easy Testing**: Functions can be easily unit tested in isolation +- **Flexible Integration**: Works seamlessly with AWS Lambda and Bedrock Agents + +## Complete Example with Dependency Injection + +```csharp +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() { - // Use the function resolver to resolve and invoke a Lambda function - var result = await _functionResolver.ResolveAndInvokeAsync("MyLambdaFunctionName", new { /* input parameters */ }); + // Set up dependency injection + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddSingleton(); + services.AddBedrockResolver(); // Extension method to register the resolver - // Process the result - context.Logger.LogLine($"Result: {result}"); + var serviceProvider = services.BuildServiceProvider(); + _resolver = serviceProvider.GetRequiredService(); + + // 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); } } } -``` \ No newline at end of file +``` + +## Learn More + +For more information about Amazon Bedrock Agents and function integration, see the [Amazon Bedrock documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-tools.html). diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs index 4e3ba51c..0b0da0af 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs @@ -1,10 +1,12 @@ using System.Globalization; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; using Microsoft.Extensions.DependencyInjection; + #pragma warning disable CS0162 // Unreachable code detected namespace AWS.Lambda.Powertools.EventHandler.Tests; @@ -23,7 +25,7 @@ public BedrockAgentFunctionResolverTests() Converters = { new JsonStringEnumConverter() } })!; } - + [Fact] public void TestFunctionHandlerWithNoParameters() { @@ -202,7 +204,7 @@ public void TestFunctionHandlerWithParameters() // Assert Assert.Equal("Hello, World!", result.Text); } - + [Fact] public void TestFunctionHandlerWithEvent() { @@ -211,28 +213,25 @@ public void TestFunctionHandlerWithEvent() resolver.Tool( name: "GetCustomForecast", description: "Get detailed forecast for a location", - handler: (string location, int days, ILambdaContext ctx) => { + handler: (string location, int days, ILambdaContext ctx) => + { ctx.Logger.LogLine($"Getting forecast for {location}"); return $"{days}-day forecast for {location}"; } ); - + resolver.Tool( name: "Greet", description: "Greet a user", - handler: (string name) => { - return $"Hello {name}"; - } + handler: (string name) => { return $"Hello {name}"; } ); - + resolver.Tool( name: "Simple", description: "Greet a user", - handler: () => { - return "Hello"; - } + handler: () => { return "Hello"; } ); - + var input = new ActionGroupInvocationInput { Function = "GetCustomForecast", @@ -252,7 +251,7 @@ public void TestFunctionHandlerWithEvent() } } }; - + var context = new TestLambdaContext(); // Act @@ -261,7 +260,7 @@ public void TestFunctionHandlerWithEvent() // Assert Assert.Equal("1-day forecast for Lisbon", result.Text); } - + [Fact] public void TestFunctionHandlerWithEventAndServices() { @@ -272,17 +271,17 @@ public void TestFunctionHandlerWithEventAndServices() var serviceProvider = services.BuildServiceProvider(); var resolver = serviceProvider.GetRequiredService(); - + resolver.Tool( name: "GetCustomForecast", description: "Get detailed forecast for a location", handler: async (string location, int days, IMyInterface client, ILambdaContext ctx) => { - var resp = await client.DoSomething(location , days); + var resp = await client.DoSomething(location, days); return resp; } ); - + var input = new ActionGroupInvocationInput { Function = "GetCustomForecast", @@ -302,7 +301,7 @@ public void TestFunctionHandlerWithEventAndServices() } } }; - + var context = new TestLambdaContext(); // Act @@ -311,7 +310,7 @@ public void TestFunctionHandlerWithEventAndServices() // Assert Assert.Equal("Forecast for Lisbon for 1 days", result.Text); } - + [Fact] public void TestFunctionHandlerWithEventTypes() { @@ -320,20 +319,19 @@ public void TestFunctionHandlerWithEventTypes() resolver.Tool( name: "GetCustomForecast", description: "Get detailed forecast for a location", - handler: (string location, int days, ILambdaContext ctx) => { + handler: (string location, int days, ILambdaContext ctx) => + { ctx.Logger.LogLine($"Getting forecast for {location}"); return $"{days}-day forecast for {location}"; } ); - + resolver.Tool( name: "Greet", description: "Greet a user", - handler: (string name) => { - return $"Hello {name}"; - } + handler: (string name) => { return $"Hello {name}"; } ); - + var input = new ActionGroupInvocationInput { Function = "GetCustomForecast", @@ -353,7 +351,7 @@ public void TestFunctionHandlerWithEventTypes() } } }; - + var context = new TestLambdaContext(); // Act @@ -362,7 +360,7 @@ public void TestFunctionHandlerWithEventTypes() // Assert Assert.Equal("1-day forecast for Lisbon", result.Text); } - + [Fact] public void TestFunctionHandlerWithBooleanParameter() { @@ -371,9 +369,7 @@ public void TestFunctionHandlerWithBooleanParameter() resolver.Tool( name: "TestBool", description: "Test boolean parameter", - handler: (bool isEnabled) => { - return $"Feature is {(isEnabled ? "enabled" : "disabled")}"; - } + handler: (bool isEnabled) => { return $"Feature is {(isEnabled ? "enabled" : "disabled")}"; } ); var input = new ActionGroupInvocationInput @@ -396,7 +392,7 @@ public void TestFunctionHandlerWithBooleanParameter() // Assert Assert.Equal("Feature is enabled", result.Text); } - + [Fact] public void TestFunctionHandlerWithMissingRequiredParameter() { @@ -420,7 +416,7 @@ public void TestFunctionHandlerWithMissingRequiredParameter() // Assert Assert.Contains("Hello, !", result.Text); } - + [Fact] public void TestFunctionHandlerWithMultipleParameterTypes() { @@ -429,7 +425,8 @@ public void TestFunctionHandlerWithMultipleParameterTypes() resolver.Tool( name: "ComplexFunction", description: "Test multiple parameter types", - handler: (string name, int count, bool isActive) => { + handler: (string name, int count, bool isActive) => + { return $"Name: {name}, Count: {count}, Active: {isActive}"; } ); @@ -451,7 +448,7 @@ public void TestFunctionHandlerWithMultipleParameterTypes() // Assert Assert.Equal("Name: Test, Count: 5, Active: True", result.Text); } - + public enum TestEnum { Option1, @@ -467,9 +464,7 @@ public void TestFunctionHandlerWithEnumParameter() resolver.Tool( name: "EnumTest", description: "Test enum parameter", - handler: (TestEnum option) => { - return $"Selected option: {option}"; - } + handler: (TestEnum option) => { return $"Selected option: {option}"; } ); var input = new ActionGroupInvocationInput @@ -492,7 +487,7 @@ public void TestFunctionHandlerWithEnumParameter() // Assert Assert.Equal("Selected option: Option2", result.Text); } - + [Fact] public void TestParameterNameCaseSensitivity() { @@ -524,7 +519,7 @@ public void TestParameterNameCaseSensitivity() // Assert Assert.Equal("Hello, John!", result.Text); } - + [Fact] public void TestParameterOrderIndependence() { @@ -533,9 +528,7 @@ public void TestParameterOrderIndependence() resolver.Tool( name: "OrderTest", description: "Test parameter order independence", - handler: (string firstName, string lastName) => { - return $"Name: {firstName} {lastName}"; - } + handler: (string firstName, string lastName) => { return $"Name: {firstName} {lastName}"; } ); var input = new ActionGroupInvocationInput @@ -555,7 +548,7 @@ public void TestParameterOrderIndependence() // Assert Assert.Equal("Name: John Smith", result.Text); } - + [Fact] public void TestFunctionHandlerWithDecimalParameter() { @@ -564,7 +557,8 @@ public void TestFunctionHandlerWithDecimalParameter() resolver.Tool( name: "PriceCalculator", description: "Calculate total price with tax", - handler: (decimal price) => { + handler: (decimal price) => + { var withTax = price * 1.2m; return $"Total price with tax: {withTax.ToString("F2", CultureInfo.InvariantCulture)}"; } @@ -590,7 +584,7 @@ public void TestFunctionHandlerWithDecimalParameter() // Assert Assert.Contains("35.99", result.Text); } - + [Fact] public void TestFunctionHandlerWithArrayParameter() { @@ -599,7 +593,8 @@ public void TestFunctionHandlerWithArrayParameter() resolver.Tool( name: "ArrayTest", description: "Test with array parameter", - handler: (string text) => { + handler: (string text) => + { // In a real implementation, you'd parse the array from the string // ActionGroupInvocationInput doesn't directly support array types return $"Received: {text}"; @@ -615,7 +610,7 @@ public void TestFunctionHandlerWithArrayParameter() { Name = "text", Value = "[\"item1\",\"item2\"]", // Array as JSON string - Type = "Array" + Type = "Array" } } }; @@ -626,7 +621,111 @@ public void TestFunctionHandlerWithArrayParameter() // Assert Assert.Equal("Received: [\"item1\",\"item2\"]", result.Text); } - + + [Fact] + public void TestFunctionHandlerWithStringArrayParameter() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "ProcessWorkout", + description: "Process workout exercises", + handler: (string[] exercises) => + { + var result = new StringBuilder(); + result.AppendLine("Your workout plan:"); + + for (int i = 0; i < exercises.Length; i++) + { + result.AppendLine($" {i + 1}. {exercises[i]}"); + } + + return result.ToString(); + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "ProcessWorkout", + Parameters = new List + { + new Parameter + { + Name = "exercises", + Value = + "[\"Squats, 3 sets of 10 reps\",\"Push-ups, 3 sets of 10 reps\",\"Plank, 3 sets of 30 seconds\"]", + Type = "String" // The type is String since it contains JSON + } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains("Your workout plan:", result.Text); + Assert.Contains("1. Squats, 3 sets of 10 reps", result.Text); + Assert.Contains("2. Push-ups, 3 sets of 10 reps", result.Text); + Assert.Contains("3. Plank, 3 sets of 30 seconds", result.Text); + } + + [Fact] + public void TestFunctionHandlerWithStringArrayParameterManualParse() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "ProcessWorkout", + description: "Process workout exercises", + handler: (ActionGroupInvocationInput input) => + { + // Manual array parsing since the resolver doesn't natively support arrays + var exercisesJson = input.Parameters.FirstOrDefault(p => p.Name == "exercises")?.Value ?? "[]"; + + // Parse JSON array + var exercises = JsonSerializer.Deserialize(exercisesJson); + + // Process the array items + var result = new StringBuilder(); + result.AppendLine("Your workout plan:"); + + if (exercises != null) + { + for (int i = 0; i < exercises.Length; i++) + { + result.AppendLine($" {i + 1}. {exercises[i]}"); + } + } + + return result.ToString(); + } + ); + + var input = new ActionGroupInvocationInput + { + Function = "ProcessWorkout", + Parameters = new List + { + new Parameter + { + Name = "exercises", + Value = + "[\"Squats, 3 sets of 10 reps\",\"Push-ups, 3 sets of 10 reps\",\"Plank, 3 sets of 30 seconds\"]", + Type = "String" // The type is still String even though it contains JSON + } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains("Your workout plan:", result.Text); + Assert.Contains("1. Squats, 3 sets of 10 reps", result.Text); + Assert.Contains("2. Push-ups, 3 sets of 10 reps", result.Text); + Assert.Contains("3. Plank, 3 sets of 30 seconds", result.Text); + } + [Fact] public void TestFunctionHandlerWithExceptionInHandler() { @@ -635,17 +734,18 @@ public void TestFunctionHandlerWithExceptionInHandler() resolver.Tool( name: "ThrowingFunction", description: "Function that throws exception", - handler: () => { + handler: () => + { throw new InvalidOperationException("Test error"); return "This will not run:"; } ); - + var input = new ActionGroupInvocationInput { Function = "ThrowingFunction" }; - + // Act var result = resolver.Resolve(input); - + // Assert Assert.Contains("Error executing function", result.Text); } diff --git a/mkdocs.yml b/mkdocs.yml index 9afa0fd3..8feab461 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,6 +26,7 @@ nav: - utilities/batch-processing.md - Event Handler: - core/event_handler/appsync_events.md + - core/event_handler/bedrock_agent_function.md - utilities/parameters.md - utilities/jmespath-functions.md - Resources: From 5a9c9328a18241054f45b1fc149dbf2febc35a20 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 26 May 2025 12:42:48 +0100 Subject: [PATCH 30/52] add new utility to version --- version.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/version.json b/version.json index 0b56b18d..759ed8a3 100644 --- a/version.json +++ b/version.json @@ -9,6 +9,7 @@ "Parameters": "1.3.1", "Idempotency": "1.3.0", "BatchProcessing": "1.2.1", - "EventHandler": "1.0.0" + "EventHandler": "1.0.0", + "BedrockAgentFunctionResolver": "1.0.0" } } From c5488303484189a9cd00db15cfe45286a80f9c35 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 27 May 2025 12:18:47 +0100 Subject: [PATCH 31/52] namespace refactor --- libraries/AWS.Lambda.Powertools.sln | 2 +- ...ler.Resolvers.BedrockAgentFunction.csproj} | 4 +- .../BedrockAgentFunctionResolver.cs | 2 +- .../BedrockAgentFunctionResolverExtensions.cs | 4 +- .../InternalsVisibleTo.cs | 0 .../ParameterAccessor.cs | 3 +- .../Readme.md | 0 ...ambda.Powertools.EventHandler.Tests.csproj | 6 ++- .../BedrockAgentFunctionResolverTests.cs | 41 ++++++++++++------- .../bedrockFunctionEvent2.json | 27 ++++++++++++ 10 files changed, 66 insertions(+), 23 deletions(-) rename libraries/src/{AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj => AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj} (71%) rename libraries/src/{AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver => AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction}/BedrockAgentFunctionResolver.cs (99%) rename libraries/src/{AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver => AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction}/BedrockAgentFunctionResolverExtensions.cs (91%) rename libraries/src/{AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver => AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction}/InternalsVisibleTo.cs (100%) rename libraries/src/{AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver => AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction}/ParameterAccessor.cs (97%) rename libraries/src/{AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver => AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction}/Readme.md (100%) create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json diff --git a/libraries/AWS.Lambda.Powertools.sln b/libraries/AWS.Lambda.Powertools.sln index 0ca62f24..056b3801 100644 --- a/libraries/AWS.Lambda.Powertools.sln +++ b/libraries/AWS.Lambda.Powertools.sln @@ -109,7 +109,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Event EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.EventHandler", "src\AWS.Lambda.Powertools.EventHandler\AWS.Lambda.Powertools.EventHandler.csproj", "{F4B8D5AF-D3CA-4910-A14D-E5BAEF0FD1DE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver", "src\AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver\AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj", "{281F7EB5-ACE5-458F-BC88-46A8899DF3BA}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction", "src\AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction\AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj", "{281F7EB5-ACE5-458F-BC88-46A8899DF3BA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj similarity index 71% rename from libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj rename to libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj index 87033029..1eacbf0f 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver.csproj +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj @@ -2,10 +2,8 @@ - AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver + AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction Powertools for AWS Lambda (.NET) - Event Handler Bedrock Agent Function Resolver package. - AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver - AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver net8.0 false enable diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs similarity index 99% rename from libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs rename to libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs index bcf61e67..7bbcbcdd 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs @@ -5,7 +5,7 @@ using Amazon.Lambda.Core; // ReSharper disable once CheckNamespace -namespace AWS.Lambda.Powertools.EventHandler +namespace AWS.Lambda.Powertools.EventHandler.Resolvers { [JsonSerializable(typeof(string[]))] [JsonSerializable(typeof(int[]))] diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs similarity index 91% rename from libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs rename to libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs index 871e01b3..47ec0cfa 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/BedrockAgentFunctionResolverExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs @@ -1,12 +1,12 @@ using Microsoft.Extensions.DependencyInjection; // ReSharper disable once CheckNamespace -namespace AWS.Lambda.Powertools.EventHandler +namespace AWS.Lambda.Powertools.EventHandler.Resolvers { /// /// Extended Bedrock Agent Function Resolver with dependency injection support. /// - public class DiBedrockAgentFunctionResolver : BedrockAgentFunctionResolver + internal class DiBedrockAgentFunctionResolver : BedrockAgentFunctionResolver { /// /// Gets the service provider used for dependency injection. diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/InternalsVisibleTo.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/InternalsVisibleTo.cs similarity index 100% rename from libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/InternalsVisibleTo.cs rename to libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/InternalsVisibleTo.cs diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/ParameterAccessor.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs similarity index 97% rename from libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/ParameterAccessor.cs rename to libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs index 51217357..eb614eb7 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/ParameterAccessor.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs @@ -1,7 +1,8 @@ using System.Globalization; using Amazon.BedrockAgentRuntime.Model; -namespace AWS.Lambda.Powertools.EventHandler; +// ReSharper disable once CheckNamespace +namespace AWS.Lambda.Powertools.EventHandler.Resolvers; /// /// Provides strongly-typed access to the parameters of an agent function call. diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md similarity index 100% rename from libraries/src/AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver/Readme.md rename to libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj index aab78861..1d0b8362 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj @@ -32,7 +32,7 @@ - + @@ -45,6 +45,10 @@ PreserveNewest + + + PreserveNewest + diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs index 0b0da0af..b0a85aad 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs @@ -9,23 +9,11 @@ #pragma warning disable CS0162 // Unreachable code detected -namespace AWS.Lambda.Powertools.EventHandler.Tests; +// ReSharper disable once CheckNamespace +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.Tests; public class BedrockAgentFunctionResolverTests { - private readonly ActionGroupInvocationInput _bedrockEvent; - - public BedrockAgentFunctionResolverTests() - { - _bedrockEvent = JsonSerializer.Deserialize( - File.ReadAllText("bedrockFunctionEvent.json"), - new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - Converters = { new JsonStringEnumConverter() } - })!; - } - [Fact] public void TestFunctionHandlerWithNoParameters() { @@ -725,6 +713,31 @@ public void TestFunctionHandlerWithStringArrayParameterManualParse() Assert.Contains("2. Push-ups, 3 sets of 10 reps", result.Text); Assert.Contains("3. Plank, 3 sets of 30 seconds", result.Text); } + + [Fact] + public async Task TestPayload2() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("getWeatherForCity", "Get weather for a specific city", async (string city, ILambdaContext context) => + { + return await Task.FromResult(city); + }); + + var input = JsonSerializer.Deserialize( + File.ReadAllText("bedrockFunctionEvent2.json"), + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() } + })!; + + // Act + var result = await resolver.ResolveAsync(input); + + // Assert + Assert.Equal("Lisbon", result.Text); + } [Fact] public void TestFunctionHandlerWithExceptionInHandler() diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json new file mode 100644 index 00000000..401be213 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json @@ -0,0 +1,27 @@ +{ + "messageVersion": "1.0", + "function": "get_weather_city", + "parameters": [ + { + "name": "month", + "type": "number", + "value": "5" + }, + { + "name": "city", + "type": "string", + "value": "London" + } + ], + "sessionId": "533568316194812", + "agent": { + "name": "powertools-function-agent", + "version": "DRAFT", + "id": "AVMWXZYN4X", + "alias": "TSTALIASID" + }, + "actionGroup": "action_group_quick_start_hgo6p", + "sessionAttributes": {}, + "promptSessionAttributes": {}, + "inputText": "weather in london?" +} \ No newline at end of file From b20528e5f04b937d8ce203241d9dc10e2472ca87 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 27 May 2025 17:05:09 +0100 Subject: [PATCH 32/52] add Bedrock function request and response models with agent support --- ...dler.Resolvers.BedrockAgentFunction.csproj | 2 +- .../BedrockAgentFunctionResolver.cs | 165 +++++---- .../Models/Agent.cs | 33 ++ .../Models/BedrockFunctionRequest.cs | 63 ++++ .../Models/BedrockFunctionResponse.cs | 64 ++++ .../Models/FunctionResponse.cs | 35 ++ .../Models/Parameter.cs | 29 ++ .../Models/Response.cs | 27 ++ .../Models/ResponseBody.cs | 15 + .../Models/TextBody.cs | 15 + .../ParameterAccessor.cs | 1 - .../BedrockAgentFunctionResolverTests.cs | 338 ++++++++++++++---- .../bedrockFunctionEvent2.json | 2 +- 13 files changed, 636 insertions(+), 153 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Agent.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionRequest.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionResponse.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/FunctionResponse.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Parameter.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Response.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/ResponseBody.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/TextBody.cs diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj index 1eacbf0f..1f5c2aea 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj @@ -9,11 +9,11 @@ enable enable true + 1.0.4 - diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs index 7bbcbcdd..072f5db5 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs @@ -1,8 +1,8 @@ using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; -using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; // ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler.Resolvers @@ -16,7 +16,7 @@ namespace AWS.Lambda.Powertools.EventHandler.Resolvers internal partial class BedrockFunctionResolverContext : JsonSerializerContext { } - + /// /// A resolver for Bedrock Agent functions that allows registering handlers for tool functions. /// @@ -27,7 +27,7 @@ internal partial class BedrockFunctionResolverContext : JsonSerializerContext /// resolver.Tool("GetWeather", (string city) => $"Weather in {city} is sunny"); /// /// // Lambda handler - /// public ActionGroupInvocationOutput FunctionHandler(ActionGroupInvocationInput input, ILambdaContext context) + /// public BedrockFunctionResponse FunctionHandler(BedrockFunctionRequest input, ILambdaContext context) /// { /// return resolver.Resolve(input, context); /// } @@ -36,7 +36,7 @@ internal partial class BedrockFunctionResolverContext : JsonSerializerContext public class BedrockAgentFunctionResolver { private readonly - Dictionary> + Dictionary> _handlers = new(); private static readonly HashSet _bedrockParameterTypes = new() @@ -58,11 +58,11 @@ private readonly }; private static bool IsBedrockParameter(Type type) => - _bedrockParameterTypes.Contains(type) || type.IsEnum || + _bedrockParameterTypes.Contains(type) || type.IsEnum || (type.IsArray && _bedrockParameterTypes.Contains(type.GetElementType()!)); /// - /// Registers a handler that directly accepts ActionGroupInvocationInput and returns ActionGroupInvocationOutput + /// Registers a handler that directly accepts BedrockFunctionRequest and returns BedrockFunctionResponse /// /// The name of the tool function /// The handler function that accepts input and context and returns output @@ -73,9 +73,9 @@ private static bool IsBedrockParameter(Type type) => /// var resolver = new BedrockAgentFunctionResolver(); /// resolver.Tool( /// "GetWeatherDetails", - /// (ActionGroupInvocationInput input, ILambdaContext context) => { + /// (BedrockFunctionRequest input, ILambdaContext context) => { /// context.Logger.LogLine($"Processing request for {input.Function}"); - /// return new ActionGroupInvocationOutput { Text = "Weather details response" }; + /// return new BedrockFunctionResponse { Text = "Weather details response" }; /// }, /// "Gets detailed weather information" /// ); @@ -83,7 +83,7 @@ private static bool IsBedrockParameter(Type type) => /// public BedrockAgentFunctionResolver Tool( string name, - Func handler, + Func handler, string description = "") { if (handler == null) @@ -94,7 +94,7 @@ public BedrockAgentFunctionResolver Tool( } /// - /// Registers a handler that directly accepts ActionGroupInvocationInput and returns ActionGroupInvocationOutput + /// Registers a handler that directly accepts BedrockFunctionRequest and returns BedrockFunctionResponse /// /// The name of the tool function /// The handler function that accepts input and returns output @@ -105,9 +105,9 @@ public BedrockAgentFunctionResolver Tool( /// var resolver = new BedrockAgentFunctionResolver(); /// resolver.Tool( /// "GetWeatherDetails", - /// (ActionGroupInvocationInput input) => { + /// (BedrockFunctionRequest input) => { /// var city = input.Parameters.FirstOrDefault(p => p.Name == "city")?.Value; - /// return new ActionGroupInvocationOutput { Text = $"Weather in {city} is sunny" }; + /// return new BedrockFunctionResponse { Text = $"Weather in {city} is sunny" }; /// }, /// "Gets weather for a city" /// ); @@ -115,7 +115,7 @@ public BedrockAgentFunctionResolver Tool( /// public BedrockAgentFunctionResolver Tool( string name, - Func handler, + Func handler, string description = "") { if (handler == null) @@ -126,7 +126,7 @@ public BedrockAgentFunctionResolver Tool( } /// - /// Registers a parameter-less handler that returns ActionGroupInvocationOutput + /// Registers a parameter-less handler that returns BedrockFunctionResponse /// /// The name of the tool function /// The handler function that returns output @@ -137,14 +137,14 @@ public BedrockAgentFunctionResolver Tool( /// var resolver = new BedrockAgentFunctionResolver(); /// resolver.Tool( /// "GetCurrentTime", - /// () => new ActionGroupInvocationOutput { Text = DateTime.Now.ToString() }, + /// () => new BedrockFunctionResponse { Text = DateTime.Now.ToString() }, /// "Gets the current server time" /// ); /// /// public BedrockAgentFunctionResolver Tool( string name, - Func handler, + Func handler, string description = "") { ArgumentNullException.ThrowIfNull(handler); @@ -177,7 +177,7 @@ public BedrockAgentFunctionResolver Tool( { ArgumentNullException.ThrowIfNull(handler); - _handlers[name] = (input, context) => new ActionGroupInvocationOutput { Text = handler() }; + _handlers[name] = (input, context) => BedrockFunctionResponse.WithText(handler(), input.ActionGroup, name); return this; } @@ -208,7 +208,7 @@ public BedrockAgentFunctionResolver Tool( _handlers[name] = (input, context) => { var result = handler(); - return ConvertToOutput(result); + return ConvertToOutput(result, input.ActionGroup, name); }; return this; } @@ -343,7 +343,7 @@ public BedrockAgentFunctionResolver Tool( { args[i] = context; } - else if (paramType == typeof(ActionGroupInvocationInput)) + else if (paramType == typeof(BedrockFunctionRequest)) { args[i] = input; } @@ -356,24 +356,30 @@ public BedrockAgentFunctionResolver Tool( if (paramType.IsArray) { var jsonArrayStr = accessor.Get(paramName); - + if (!string.IsNullOrEmpty(jsonArrayStr)) { try { // AOT-compatible deserialization using source generation if (paramType == typeof(string[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.StringArray); + args[i] = JsonSerializer.Deserialize(jsonArrayStr, + BedrockFunctionResolverContext.Default.StringArray); else if (paramType == typeof(int[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.Int32Array); + args[i] = JsonSerializer.Deserialize(jsonArrayStr, + BedrockFunctionResolverContext.Default.Int32Array); else if (paramType == typeof(long[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.Int64Array); + args[i] = JsonSerializer.Deserialize(jsonArrayStr, + BedrockFunctionResolverContext.Default.Int64Array); else if (paramType == typeof(double[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.DoubleArray); + args[i] = JsonSerializer.Deserialize(jsonArrayStr, + BedrockFunctionResolverContext.Default.DoubleArray); else if (paramType == typeof(bool[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.BooleanArray); + args[i] = JsonSerializer.Deserialize(jsonArrayStr, + BedrockFunctionResolverContext.Default.BooleanArray); else if (paramType == typeof(decimal[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.DecimalArray); + args[i] = JsonSerializer.Deserialize(jsonArrayStr, + BedrockFunctionResolverContext.Default.DecimalArray); else args[i] = null; // Unsupported array type } @@ -387,6 +393,7 @@ public BedrockAgentFunctionResolver Tool( args[i] = null; } } + if (paramType == typeof(string)) args[i] = accessor.Get(paramName); else if (paramType == typeof(int)) @@ -424,48 +431,47 @@ public BedrockAgentFunctionResolver Tool( // Execute the handler var result = handler.DynamicInvoke(args); - // Direct return for ActionGroupInvocationOutput - if (result is ActionGroupInvocationOutput output) + // Direct return for BedrockFunctionResponse + if (result is BedrockFunctionResponse output) return output; // Handle async results with specific type checks (AOT-compatible) - if (result is Task outputTask) + if (result is Task outputTask) return outputTask.Result; if (result is Task stringTask) - return ConvertToOutput((TResult)(object)stringTask.Result); + return ConvertToOutput((TResult)(object)stringTask.Result, input.ActionGroup, name); if (result is Task intTask) - return ConvertToOutput((TResult)(object)intTask.Result); + return ConvertToOutput((TResult)(object)intTask.Result, input.ActionGroup, name); if (result is Task boolTask) - return ConvertToOutput((TResult)(object)boolTask.Result); + return ConvertToOutput((TResult)(object)boolTask.Result, input.ActionGroup, name); if (result is Task doubleTask) - return ConvertToOutput((TResult)(object)doubleTask.Result); + return ConvertToOutput((TResult)(object)doubleTask.Result, input.ActionGroup, name); if (result is Task longTask) - return ConvertToOutput((TResult)(object)longTask.Result); + return ConvertToOutput((TResult)(object)longTask.Result, input.ActionGroup, name); if (result is Task decimalTask) - return ConvertToOutput((TResult)(object)decimalTask.Result); + return ConvertToOutput((TResult)(object)decimalTask.Result, input.ActionGroup, name); if (result is Task dateTimeTask) - return ConvertToOutput((TResult)(object)dateTimeTask.Result); + return ConvertToOutput((TResult)(object)dateTimeTask.Result, input.ActionGroup, name); if (result is Task guidTask) - return ConvertToOutput((TResult)(object)guidTask.Result); + return ConvertToOutput((TResult)(object)guidTask.Result, input.ActionGroup, name); if (result is Task objectTask) - return ConvertToOutput((TResult)objectTask.Result!); + return ConvertToOutput((TResult)objectTask.Result!, input.ActionGroup, name); // For regular Task with no result if (result is Task task) { task.GetAwaiter().GetResult(); - return new ActionGroupInvocationOutput { Text = string.Empty }; + return BedrockFunctionResponse.WithText(string.Empty, input.ActionGroup, name); } - return ConvertToOutput((TResult)result!); + return ConvertToOutput(result, input.ActionGroup, name); } catch (Exception ex) { - context?.Logger.LogError($"Error executing function {name}: {ex.Message}"); - return new ActionGroupInvocationOutput - { - Text = $"Error executing function: {ex.Message}" - }; + context?.Logger.LogError(ex.ToString()); + var innerException = ex.InnerException ?? ex; + return BedrockFunctionResponse.WithText($"Error executing function: {innerException.Message}", + input.ActionGroup, name); } }; @@ -481,7 +487,7 @@ public BedrockAgentFunctionResolver Tool( /// /// /// // Lambda handler - /// public ActionGroupInvocationOutput FunctionHandler(ActionGroupInvocationInput input, ILambdaContext context) + /// public BedrockFunctionResponse FunctionHandler(BedrockFunctionRequest input, ILambdaContext context) /// { /// var resolver = new BedrockAgentFunctionResolver() /// .Tool("GetWeather", (string city) => $"Weather in {city} is sunny") @@ -491,7 +497,7 @@ public BedrockAgentFunctionResolver Tool( /// } /// /// - public ActionGroupInvocationOutput Resolve(ActionGroupInvocationInput input, ILambdaContext? context = null) + public BedrockFunctionResponse Resolve(BedrockFunctionRequest input, ILambdaContext? context = null) { return ResolveAsync(input, context).GetAwaiter().GetResult(); } @@ -505,7 +511,7 @@ public ActionGroupInvocationOutput Resolve(ActionGroupInvocationInput input, ILa /// /// /// // Async Lambda handler - /// public async Task<ActionGroupInvocationOutput> FunctionHandler(ActionGroupInvocationInput input, ILambdaContext context) + /// public async Task<BedrockFunctionResponse> FunctionHandler(BedrockFunctionRequest input, ILambdaContext context) /// { /// var resolver = new BedrockAgentFunctionResolver() /// .Tool("GetWeatherAsync", async (string city) => { @@ -519,20 +525,17 @@ public ActionGroupInvocationOutput Resolve(ActionGroupInvocationInput input, ILa /// } /// /// - public async Task ResolveAsync(ActionGroupInvocationInput input, + public async Task ResolveAsync(BedrockFunctionRequest input, ILambdaContext? context = null) { return await Task.FromResult(HandleEvent(input, context)); } - private ActionGroupInvocationOutput HandleEvent(ActionGroupInvocationInput input, ILambdaContext? context) + private BedrockFunctionResponse HandleEvent(BedrockFunctionRequest input, ILambdaContext? context) { if (string.IsNullOrEmpty(input.Function)) { - return new ActionGroupInvocationOutput - { - Text = "No function specified in the request" - }; + return BedrockFunctionResponse.WithText("No function specified in the request", input.ActionGroup, ""); } if (_handlers.TryGetValue(input.Function, out var handler)) @@ -543,67 +546,77 @@ private ActionGroupInvocationOutput HandleEvent(ActionGroupInvocationInput input } catch (Exception ex) { - context?.Logger.LogError($"Error executing function {input.Function}: {ex.Message}"); - return new ActionGroupInvocationOutput - { - Text = $"Error executing function: {ex.Message}" - }; + context?.Logger.LogError(ex.ToString()); + return BedrockFunctionResponse.WithText($"Error executing function: {ex.Message}", input.ActionGroup, + input.Function); } } context?.Logger.LogWarning($"No handler registered for function: {input.Function}"); - return new ActionGroupInvocationOutput - { - Text = $"No handler registered for function: {input.Function}" - }; + return BedrockFunctionResponse.WithText($"No handler registered for function: {input.Function}", + input.ActionGroup, input.Function); } - private ActionGroupInvocationOutput ConvertToOutput(T result) + private BedrockFunctionResponse ConvertToOutput(T result, string actionGroup, string function) { if (result == null) { - return new ActionGroupInvocationOutput { Text = string.Empty }; + return BedrockFunctionResponse.WithText(string.Empty, actionGroup, function); } - // If result is already an ActionGroupInvocationOutput, return it directly - if (result is ActionGroupInvocationOutput output) + // If result is already an BedrockFunctionResponse, ensure action group and function are set + if (result is BedrockFunctionResponse output) { + // If the action group or function are not set in the output, use the provided values + if (string.IsNullOrEmpty(output.Response.ActionGroup)) + { + output.Response.ActionGroup = actionGroup; + } + + if (string.IsNullOrEmpty(output.Response.Function)) + { + output.Response.Function = function; + } + return output; } // For primitive types and strings, convert to string if (result is string str) { - return new ActionGroupInvocationOutput { Text = str }; + return BedrockFunctionResponse.WithText(str, actionGroup, function); } if (result is int intVal) { - return new ActionGroupInvocationOutput { Text = intVal.ToString(CultureInfo.InvariantCulture) }; + return BedrockFunctionResponse.WithText(intVal.ToString(CultureInfo.InvariantCulture), actionGroup, function); } if (result is double doubleVal) { - return new ActionGroupInvocationOutput { Text = doubleVal.ToString(CultureInfo.InvariantCulture) }; + return BedrockFunctionResponse.WithText(doubleVal.ToString(CultureInfo.InvariantCulture), actionGroup, + function); } if (result is bool boolVal) { - return new ActionGroupInvocationOutput { Text = boolVal.ToString() }; + return BedrockFunctionResponse.WithText(boolVal.ToString(), actionGroup, function); } if (result is long longVal) { - return new ActionGroupInvocationOutput { Text = longVal.ToString(CultureInfo.InvariantCulture) }; + return BedrockFunctionResponse.WithText(longVal.ToString(CultureInfo.InvariantCulture), actionGroup, + function); } if (result is decimal decimalVal) { - return new ActionGroupInvocationOutput { Text = decimalVal.ToString(CultureInfo.InvariantCulture) }; + return BedrockFunctionResponse.WithText(decimalVal.ToString(CultureInfo.InvariantCulture), actionGroup, + function); } - // For any other type, use ToString() instead of JSON serialization - return new ActionGroupInvocationOutput { Text = result.ToString() ?? string.Empty }; + // For any other type, use ToString() + return BedrockFunctionResponse.WithText(result.ToString() ?? string.Empty, actionGroup, function); } } -} +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Agent.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Agent.cs new file mode 100644 index 00000000..5e13b2fe --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Agent.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +/// +/// Represents an agent in the Bedrock Agent function input. +/// +public class Agent +{ + /// + /// Gets or sets the name of the agent. + /// + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the version of the agent. + /// + [JsonPropertyName("version")] + public string Version { get; set; } = string.Empty; + + /// + /// Gets or sets the ID of the agent. + /// + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// + /// Gets or sets the alias of the agent. + /// + [JsonPropertyName("alias")] + public string Alias { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionRequest.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionRequest.cs new file mode 100644 index 00000000..bd2d8395 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionRequest.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +/// +/// Represents the input for a Bedrock Agent function. +/// +public class BedrockFunctionRequest +{ + /// + /// Gets or sets the message version. + /// + [JsonPropertyName("messageVersion")] + public string MessageVersion { get; set; } = "1.0"; + + /// + /// Gets or sets the function name. + /// + [JsonPropertyName("function")] + public string Function { get; set; } = string.Empty; + + /// + /// Gets or sets the parameters for the function. + /// + [JsonPropertyName("parameters")] + public List Parameters { get; set; } = new List(); + + /// + /// Gets or sets the session ID. + /// + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// + /// Gets or sets the agent information. + /// + [JsonPropertyName("agent")] + public Agent? Agent { get; set; } + + /// + /// Gets or sets the action group. + /// + [JsonPropertyName("actionGroup")] + public string ActionGroup { get; set; } = string.Empty; + + /// + /// Gets or sets the session attributes. + /// + [JsonPropertyName("sessionAttributes")] + public Dictionary SessionAttributes { get; set; } = new Dictionary(); + + /// + /// Gets or sets the prompt session attributes. + /// + [JsonPropertyName("promptSessionAttributes")] + public Dictionary PromptSessionAttributes { get; set; } = new Dictionary(); + + /// + /// Gets or sets the input text. + /// + [JsonPropertyName("inputText")] + public string InputText { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionResponse.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionResponse.cs new file mode 100644 index 00000000..a86df718 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionResponse.cs @@ -0,0 +1,64 @@ +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +/// +/// The version of the message that identifies the format of the event data going into the Lambda function and the expected format of the response from a Lambda function. Amazon Bedrock only supports version 1.0. +/// +public class BedrockFunctionResponse +{ + /// + /// Gets or sets the message version. + /// + [JsonPropertyName("messageVersion")] + public string MessageVersion { get; } = "1.0"; + + /// + /// Gets or sets the response. + /// + [JsonPropertyName("response")] + public Response Response { get; set; } = new Response(); + + /// + /// Contains session attributes and their values. For more information, Session and prompt session attributes. + /// + [JsonPropertyName("sessionAttributes")] + public Dictionary SessionAttributes { get; set; } = new Dictionary(); + + /// + /// Contains prompt attributes and their values. For more information, Session and prompt session attributes. + /// + [JsonPropertyName("promptSessionAttributes")] + public Dictionary PromptSessionAttributes { get; set; } = new Dictionary(); + + /// + /// Contains a list of query configurations for knowledge bases attached to the agent. For more information, Knowledge base retrieval configurations. + /// + [JsonPropertyName("knowledgeBasesConfiguration")] + public Dictionary KnowledgeBasesConfiguration { get; set; } = new Dictionary(); + + + /// + /// Creates a new instance of BedrockFunctionResponse with the specified text. + /// + public static BedrockFunctionResponse WithText(string text, string actionGroup = "", string function = "") + { + return new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = actionGroup, + Function = function, + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = text } + } + } + }, + SessionAttributes = new Dictionary(), + PromptSessionAttributes = new Dictionary() + }; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/FunctionResponse.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/FunctionResponse.cs new file mode 100644 index 00000000..e22c97d6 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/FunctionResponse.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; +// ReSharper disable InconsistentNaming +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +/// +/// Represents the function response part of a Response. +/// +public class FunctionResponse +{ + /// + /// Contains an object that defines the response from execution of the function. The key is the content type (currently only TEXT is supported) and the value is an object containing the body of the response. + /// + [JsonPropertyName("responseBody")] + public ResponseBody ResponseBody { get; set; } = new ResponseBody(); + + /// + /// (Optional) – Set to one of the following states to define the agent's behavior after processing the action: + /// + /// FAILURE – The agent throws a DependencyFailedException for the current session. Applies when the function execution fails because of a dependency failure. + /// REPROMPT – The agent passes a response string to the model to reprompt it. Applies when the function execution fails because of invalid input. + /// + [JsonPropertyName("responseState")] + public ResponseState ResponseState { get; set; } +} + +/// +/// Represents the response state of a function response. +/// +public enum ResponseState +{ + FAILURE, + REPROMPT +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Parameter.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Parameter.cs new file mode 100644 index 00000000..5e5a65ee --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Parameter.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +// ReSharper disable once CheckNamespace +namespace AWS.Lambda.Powertools.EventHandler.Resolvers +{ + /// + /// Represents a parameter for a Bedrock Agent function. + /// + public class Parameter + { + /// + /// Gets or sets the name of the parameter. + /// + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the type of the parameter. + /// + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + /// + /// Gets or sets the value of the parameter. + /// + [JsonPropertyName("value")] + public string Value { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Response.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Response.cs new file mode 100644 index 00000000..5d2e76a7 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Response.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +/// +/// Represents the response part of an BedrockFunctionResponse. +/// +public class Response +{ + /// + /// Gets or sets the action group. + /// + [JsonPropertyName("actionGroup")] + public string ActionGroup { get; internal set; } = string.Empty; + + /// + /// Gets or sets the function. + /// + [JsonPropertyName("function")] + public string Function { get; internal set; } = string.Empty; + + /// + /// Gets or sets the function response. + /// + [JsonPropertyName("functionResponse")] + public FunctionResponse FunctionResponse { get; set; } = new FunctionResponse(); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/ResponseBody.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/ResponseBody.cs new file mode 100644 index 00000000..20bc59c2 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/ResponseBody.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +/// +/// Represents the response body part of a FunctionResponse. +/// +public class ResponseBody +{ + /// + /// Gets or sets the text body. + /// + [JsonPropertyName("TEXT")] + public TextBody Text { get; set; } = new TextBody(); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/TextBody.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/TextBody.cs new file mode 100644 index 00000000..8e9a41c7 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/TextBody.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +/// +/// Represents the text body part of a ResponseBody. +/// +public class TextBody +{ + /// + /// Gets or sets the body text. + /// + [JsonPropertyName("body")] + public string Body { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs index eb614eb7..bc8001db 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs @@ -1,5 +1,4 @@ using System.Globalization; -using Amazon.BedrockAgentRuntime.Model; // ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler.Resolvers; diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs index b0a85aad..28100fe3 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs @@ -2,9 +2,9 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; using Microsoft.Extensions.DependencyInjection; #pragma warning disable CS0162 // Unreachable code detected @@ -19,16 +19,30 @@ public void TestFunctionHandlerWithNoParameters() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + resolver.Tool("TestFunction", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello, World!" } + } + } + } + }); - var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var input = new BedrockFunctionRequest { Function = "TestFunction" }; var context = new TestLambdaContext(); // Act var result = resolver.Resolve(input, context); // Assert - Assert.Equal("Hello, World!", result.Text); + Assert.Equal("Hello, World!", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -36,16 +50,30 @@ public async Task TestFunctionHandlerWithNoParametersAsync() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + resolver.Tool("TestFunction", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello, World!" } + } + } + } + }); - var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var input = new BedrockFunctionRequest { Function = "TestFunction" }; var context = new TestLambdaContext(); // Act var result = await resolver.ResolveAsync(input, context); // Assert - Assert.Equal("Hello, World!", result.Text); + Assert.Equal("Hello, World!", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -53,17 +81,31 @@ public void TestFunctionHandlerWithDescription() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }, + resolver.Tool("TestFunction", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello, World!" } + } + } + } + }, "This is a test function"); - var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var input = new BedrockFunctionRequest { Function = "TestFunction" }; var context = new TestLambdaContext(); // Act var result = resolver.Resolve(input, context); // Assert - Assert.Equal("Hello, World!", result.Text); + Assert.Equal("Hello, World!", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -71,11 +113,40 @@ public void TestFunctionHandlerWithMultiplTools() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction1", () => new ActionGroupInvocationOutput { Text = "Hello from Function 1!" }); - resolver.Tool("TestFunction2", () => new ActionGroupInvocationOutput { Text = "Hello from Function 2!" }); + + resolver.Tool("TestFunction1", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello from Function 1!" } + } + } + } + }); + resolver.Tool("TestFunction2", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello from Function 2!" } + } + } + } + }); - var input1 = new ActionGroupInvocationInput { Function = "TestFunction1" }; - var input2 = new ActionGroupInvocationInput { Function = "TestFunction2" }; + var input1 = new BedrockFunctionRequest { Function = "TestFunction1" }; + var input2 = new BedrockFunctionRequest { Function = "TestFunction2" }; var context = new TestLambdaContext(); // Act @@ -83,8 +154,57 @@ public void TestFunctionHandlerWithMultiplTools() var result2 = resolver.Resolve(input2, context); // Assert - Assert.Equal("Hello from Function 1!", result1.Text); - Assert.Equal("Hello from Function 2!", result2.Text); + Assert.Equal("Hello from Function 1!", result1.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("Hello from Function 2!", result2.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void TestFunctionHandlerWithMultiplToolsDuplicate() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("TestFunction1", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello from Function 1!" } + } + } + } + }); + resolver.Tool("TestFunction1", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello from Function 2!" } + } + } + } + }); + + var input1 = new BedrockFunctionRequest { Function = "TestFunction1" }; + var input2 = new BedrockFunctionRequest { Function = "TestFunction1" }; + var context = new TestLambdaContext(); + + // Act + var result1 = resolver.Resolve(input1, context); + var result2 = resolver.Resolve(input2, context); + + // Assert + Assert.Equal("Hello from Function 2!", result1.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("Hello from Function 2!", result2.Response.FunctionResponse.ResponseBody.Text.Body); } @@ -94,16 +214,30 @@ public void TestFunctionHandlerWithInput() // Arrange var resolver = new BedrockAgentFunctionResolver(); resolver.Tool("TestFunction", - (input, context) => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); + (input, context) => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = $"Hello, {input.Function}!" } + } + } + } + }); - var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var input = new BedrockFunctionRequest { Function = "TestFunction" }; var context = new TestLambdaContext(); // Act var result = resolver.Resolve(input, context); // Assert - Assert.Equal("Hello, TestFunction!", result.Text); + Assert.Equal("Hello, TestFunction!", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -112,16 +246,30 @@ public async Task TestFunctionHandlerWithInputAsync() // Arrange var resolver = new BedrockAgentFunctionResolver(); resolver.Tool("TestFunction", - input => new ActionGroupInvocationOutput { Text = $"Hello, {input.Function}!" }); + input => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = $"Hello, {input.Function}!" } + } + } + } + }); - var input = new ActionGroupInvocationInput { Function = "TestFunction" }; + var input = new BedrockFunctionRequest { Function = "TestFunction" }; var context = new TestLambdaContext(); // Act var result = await resolver.ResolveAsync(input, context); // Assert - Assert.Equal("Hello, TestFunction!", result.Text); + Assert.Equal("Hello, TestFunction!", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -129,16 +277,31 @@ public void TestFunctionHandlerNoToolMatch() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + resolver.Tool("TestFunction", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello, World!" } + } + } + } + }); - var input = new ActionGroupInvocationInput { Function = "NonExistentFunction" }; + var input = new BedrockFunctionRequest { Function = "NonExistentFunction" }; var context = new TestLambdaContext(); // Act var result = resolver.Resolve(input, context); // Assert - Assert.Equal("No handler registered for function: NonExistentFunction", result.Text); + Assert.Equal("No handler registered for function: NonExistentFunction", + result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -146,16 +309,31 @@ public async Task TestFunctionHandlerNoToolMatchAsync() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + resolver.Tool("TestFunction", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello, World!" } + } + } + } + }); - var input = new ActionGroupInvocationInput { Function = "NonExistentFunction" }; + var input = new BedrockFunctionRequest { Function = "NonExistentFunction" }; var context = new TestLambdaContext(); // Act var result = await resolver.ResolveAsync(input, context); // Assert - Assert.Equal("No handler registered for function: NonExistentFunction", result.Text); + Assert.Equal("No handler registered for function: NonExistentFunction", + result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -163,9 +341,23 @@ public void TestFunctionHandlerWithParameters() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new ActionGroupInvocationOutput { Text = "Hello, World!" }); + resolver.Tool("TestFunction", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "TestFunction", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Hello, World!" } + } + } + } + }); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "TestFunction", Parameters = new List @@ -190,7 +382,7 @@ public void TestFunctionHandlerWithParameters() var result = resolver.Resolve(input, context); // Assert - Assert.Equal("Hello, World!", result.Text); + Assert.Equal("Hello, World!", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -220,7 +412,7 @@ public void TestFunctionHandlerWithEvent() handler: () => { return "Hello"; } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "GetCustomForecast", Parameters = new List @@ -246,7 +438,7 @@ public void TestFunctionHandlerWithEvent() var result = resolver.Resolve(input, context); // Assert - Assert.Equal("1-day forecast for Lisbon", result.Text); + Assert.Equal("1-day forecast for Lisbon", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -270,7 +462,7 @@ public void TestFunctionHandlerWithEventAndServices() } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "GetCustomForecast", Parameters = new List @@ -296,7 +488,7 @@ public void TestFunctionHandlerWithEventAndServices() var result = resolver.Resolve(input, context); // Assert - Assert.Equal("Forecast for Lisbon for 1 days", result.Text); + Assert.Equal("Forecast for Lisbon for 1 days", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -320,7 +512,7 @@ public void TestFunctionHandlerWithEventTypes() handler: (string name) => { return $"Hello {name}"; } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "GetCustomForecast", Parameters = new List @@ -346,7 +538,7 @@ public void TestFunctionHandlerWithEventTypes() var result = resolver.Resolve(input, context); // Assert - Assert.Equal("1-day forecast for Lisbon", result.Text); + Assert.Equal("1-day forecast for Lisbon", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -360,7 +552,7 @@ public void TestFunctionHandlerWithBooleanParameter() handler: (bool isEnabled) => { return $"Feature is {(isEnabled ? "enabled" : "disabled")}"; } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "TestBool", Parameters = new List @@ -378,7 +570,7 @@ public void TestFunctionHandlerWithBooleanParameter() var result = resolver.Resolve(input); // Assert - Assert.Equal("Feature is enabled", result.Text); + Assert.Equal("Feature is enabled", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -392,7 +584,7 @@ public void TestFunctionHandlerWithMissingRequiredParameter() handler: (string name) => $"Hello, {name}!" ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "RequiredParam", Parameters = new List() // Empty parameters @@ -402,7 +594,7 @@ public void TestFunctionHandlerWithMissingRequiredParameter() var result = resolver.Resolve(input); // Assert - Assert.Contains("Hello, !", result.Text); + Assert.Contains("Hello, !", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -419,7 +611,7 @@ public void TestFunctionHandlerWithMultipleParameterTypes() } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "ComplexFunction", Parameters = new List @@ -434,7 +626,7 @@ public void TestFunctionHandlerWithMultipleParameterTypes() var result = resolver.Resolve(input); // Assert - Assert.Equal("Name: Test, Count: 5, Active: True", result.Text); + Assert.Equal("Name: Test, Count: 5, Active: True", result.Response.FunctionResponse.ResponseBody.Text.Body); } public enum TestEnum @@ -455,7 +647,7 @@ public void TestFunctionHandlerWithEnumParameter() handler: (TestEnum option) => { return $"Selected option: {option}"; } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "EnumTest", Parameters = new List @@ -473,7 +665,7 @@ public void TestFunctionHandlerWithEnumParameter() var result = resolver.Resolve(input); // Assert - Assert.Equal("Selected option: Option2", result.Text); + Assert.Equal("Selected option: Option2", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -487,7 +679,7 @@ public void TestParameterNameCaseSensitivity() handler: (string userName) => $"Hello, {userName}!" ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "CaseTest", Parameters = new List @@ -505,7 +697,7 @@ public void TestParameterNameCaseSensitivity() var result = resolver.Resolve(input); // Assert - Assert.Equal("Hello, John!", result.Text); + Assert.Equal("Hello, John!", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -519,7 +711,7 @@ public void TestParameterOrderIndependence() handler: (string firstName, string lastName) => { return $"Name: {firstName} {lastName}"; } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "OrderTest", Parameters = new List @@ -534,7 +726,7 @@ public void TestParameterOrderIndependence() var result = resolver.Resolve(input); // Assert - Assert.Equal("Name: John Smith", result.Text); + Assert.Equal("Name: John Smith", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -552,7 +744,7 @@ public void TestFunctionHandlerWithDecimalParameter() } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "PriceCalculator", Parameters = new List @@ -570,7 +762,7 @@ public void TestFunctionHandlerWithDecimalParameter() var result = resolver.Resolve(input); // Assert - Assert.Contains("35.99", result.Text); + Assert.Contains("35.99", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -584,12 +776,12 @@ public void TestFunctionHandlerWithArrayParameter() handler: (string text) => { // In a real implementation, you'd parse the array from the string - // ActionGroupInvocationInput doesn't directly support array types + // BedrockFunctionRequest doesn't directly support array types return $"Received: {text}"; } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "ArrayTest", Parameters = new List @@ -607,7 +799,7 @@ public void TestFunctionHandlerWithArrayParameter() var result = resolver.Resolve(input); // Assert - Assert.Equal("Received: [\"item1\",\"item2\"]", result.Text); + Assert.Equal("Received: [\"item1\",\"item2\"]", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -632,7 +824,7 @@ public void TestFunctionHandlerWithStringArrayParameter() } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "ProcessWorkout", Parameters = new List @@ -651,10 +843,10 @@ public void TestFunctionHandlerWithStringArrayParameter() var result = resolver.Resolve(input); // Assert - Assert.Contains("Your workout plan:", result.Text); - Assert.Contains("1. Squats, 3 sets of 10 reps", result.Text); - Assert.Contains("2. Push-ups, 3 sets of 10 reps", result.Text); - Assert.Contains("3. Plank, 3 sets of 30 seconds", result.Text); + Assert.Contains("Your workout plan:", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Contains("1. Squats, 3 sets of 10 reps", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Contains("2. Push-ups, 3 sets of 10 reps", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Contains("3. Plank, 3 sets of 30 seconds", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -665,7 +857,7 @@ public void TestFunctionHandlerWithStringArrayParameterManualParse() resolver.Tool( name: "ProcessWorkout", description: "Process workout exercises", - handler: (ActionGroupInvocationInput input) => + handler: (BedrockFunctionRequest input) => { // Manual array parsing since the resolver doesn't natively support arrays var exercisesJson = input.Parameters.FirstOrDefault(p => p.Name == "exercises")?.Value ?? "[]"; @@ -689,7 +881,7 @@ public void TestFunctionHandlerWithStringArrayParameterManualParse() } ); - var input = new ActionGroupInvocationInput + var input = new BedrockFunctionRequest { Function = "ProcessWorkout", Parameters = new List @@ -708,23 +900,21 @@ public void TestFunctionHandlerWithStringArrayParameterManualParse() var result = resolver.Resolve(input); // Assert - Assert.Contains("Your workout plan:", result.Text); - Assert.Contains("1. Squats, 3 sets of 10 reps", result.Text); - Assert.Contains("2. Push-ups, 3 sets of 10 reps", result.Text); - Assert.Contains("3. Plank, 3 sets of 30 seconds", result.Text); + Assert.Contains("Your workout plan:", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Contains("1. Squats, 3 sets of 10 reps", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Contains("2. Push-ups, 3 sets of 10 reps", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Contains("3. Plank, 3 sets of 30 seconds", result.Response.FunctionResponse.ResponseBody.Text.Body); } - + [Fact] public async Task TestPayload2() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("getWeatherForCity", "Get weather for a specific city", async (string city, ILambdaContext context) => - { - return await Task.FromResult(city); - }); + resolver.Tool("get_weather_city", "Get weather for a specific city", + async (string city, ILambdaContext context) => { return await Task.FromResult(city); }); - var input = JsonSerializer.Deserialize( + var input = JsonSerializer.Deserialize( File.ReadAllText("bedrockFunctionEvent2.json"), new JsonSerializerOptions { @@ -736,7 +926,7 @@ public async Task TestPayload2() var result = await resolver.ResolveAsync(input); // Assert - Assert.Equal("Lisbon", result.Text); + Assert.Equal("Lisbon", result.Response.FunctionResponse.ResponseBody.Text.Body); } [Fact] @@ -754,13 +944,13 @@ public void TestFunctionHandlerWithExceptionInHandler() } ); - var input = new ActionGroupInvocationInput { Function = "ThrowingFunction" }; + var input = new BedrockFunctionRequest { Function = "ThrowingFunction" }; // Act var result = resolver.Resolve(input); // Assert - Assert.Contains("Error executing function", result.Text); + Assert.Contains("Error executing function", result.Response.FunctionResponse.ResponseBody.Text.Body); } } diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json index 401be213..18e21694 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json @@ -10,7 +10,7 @@ { "name": "city", "type": "string", - "value": "London" + "value": "Lisbon" } ], "sessionId": "533568316194812", From 589e74207958796ccbd9ed325b11c7b61b080a58 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 27 May 2025 19:14:39 +0100 Subject: [PATCH 33/52] enhance Bedrock function resolver with tool registration limits and session attribute handling --- .../BedrockAgentFunctionResolver.cs | 219 +++++++-- .../BedrockAgentFunctionResolverExtensions.cs | 15 + .../BedrockFunctionResolverContext.cs | 29 ++ .../Models/Agent.cs | 17 +- .../Models/BedrockFunctionRequest.cs | 34 +- .../Models/BedrockFunctionResponse.cs | 32 +- .../Models/FunctionResponse.cs | 19 +- .../Models/Parameter.cs | 15 + .../Models/Response.cs | 15 + .../Models/ResponseBody.cs | 15 + .../Models/TextBody.cs | 15 + .../ParameterAccessor.cs | 15 + .../BedrockAgentFunctionResolverTests.cs | 459 +++++++----------- 13 files changed, 551 insertions(+), 348 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockFunctionResolverContext.cs diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs index 072f5db5..d8e08bb0 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs @@ -1,22 +1,26 @@ -using System.Globalization; +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Globalization; using System.Text.Json; -using System.Text.Json.Serialization; using Amazon.Lambda.Core; using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; // ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler.Resolvers { - [JsonSerializable(typeof(string[]))] - [JsonSerializable(typeof(int[]))] - [JsonSerializable(typeof(long[]))] - [JsonSerializable(typeof(double[]))] - [JsonSerializable(typeof(bool[]))] - [JsonSerializable(typeof(decimal[]))] - internal partial class BedrockFunctionResolverContext : JsonSerializerContext - { - } - /// /// A resolver for Bedrock Agent functions that allows registering handlers for tool functions. /// @@ -35,6 +39,8 @@ internal partial class BedrockFunctionResolverContext : JsonSerializerContext /// public class BedrockAgentFunctionResolver { + private const int MaxTools = 5; + private readonly Dictionary> _handlers = new(); @@ -61,6 +67,28 @@ private static bool IsBedrockParameter(Type type) => _bedrockParameterTypes.Contains(type) || type.IsEnum || (type.IsArray && _bedrockParameterTypes.Contains(type.GetElementType()!)); + /// + /// Checks if another tool can be registered, and logs a warning if the maximum limit is reached + /// or if a tool with the same name is already registered + /// + /// The name of the tool being registered + /// True if the tool can be registered, false if the maximum limit is reached + private bool CanRegisterTool(string name) + { + if (_handlers.Count >= MaxTools && !_handlers.ContainsKey(name)) + { + Console.WriteLine($"WARNING: Maximum number of tools ({MaxTools}) reached. Tool '{name}' will not be registered."); + return false; + } + + if (_handlers.ContainsKey(name)) + { + Console.WriteLine($"WARNING: Tool {name} already registered. Overwriting with new definition."); + } + + return true; + } + /// /// Registers a handler that directly accepts BedrockFunctionRequest and returns BedrockFunctionResponse /// @@ -89,6 +117,9 @@ public BedrockAgentFunctionResolver Tool( if (handler == null) throw new ArgumentNullException(nameof(handler)); + if (!CanRegisterTool(name)) + return this; + _handlers[name] = handler; return this; } @@ -121,6 +152,9 @@ public BedrockAgentFunctionResolver Tool( if (handler == null) throw new ArgumentNullException(nameof(handler)); + if (!CanRegisterTool(name)) + return this; + _handlers[name] = (input, _) => handler(input); return this; } @@ -149,6 +183,9 @@ public BedrockAgentFunctionResolver Tool( { ArgumentNullException.ThrowIfNull(handler); + if (!CanRegisterTool(name)) + return this; + _handlers[name] = (input, context) => handler(); return this; } @@ -177,7 +214,16 @@ public BedrockAgentFunctionResolver Tool( { ArgumentNullException.ThrowIfNull(handler); - _handlers[name] = (input, context) => BedrockFunctionResponse.WithText(handler(), input.ActionGroup, name); + if (!CanRegisterTool(name)) + return this; + + _handlers[name] = (input, context) => BedrockFunctionResponse.WithText( + handler(), + input.ActionGroup, + name, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); return this; } @@ -205,10 +251,13 @@ public BedrockAgentFunctionResolver Tool( { ArgumentNullException.ThrowIfNull(handler); + if (!CanRegisterTool(name)) + return this; + _handlers[name] = (input, context) => { var result = handler(); - return ConvertToOutput(result, input.ActionGroup, name); + return ConvertToOutput(result, input); }; return this; } @@ -323,6 +372,9 @@ public BedrockAgentFunctionResolver Tool( if (handler == null) throw new ArgumentNullException(nameof(handler)); + if (!CanRegisterTool(name)) + return this; + _handlers[name] = (input, context) => { var accessor = new ParameterAccessor(input.Parameters); @@ -439,39 +491,50 @@ public BedrockAgentFunctionResolver Tool( if (result is Task outputTask) return outputTask.Result; if (result is Task stringTask) - return ConvertToOutput((TResult)(object)stringTask.Result, input.ActionGroup, name); + return ConvertToOutput((TResult)(object)stringTask.Result, input); if (result is Task intTask) - return ConvertToOutput((TResult)(object)intTask.Result, input.ActionGroup, name); + return ConvertToOutput((TResult)(object)intTask.Result, input); if (result is Task boolTask) - return ConvertToOutput((TResult)(object)boolTask.Result, input.ActionGroup, name); + return ConvertToOutput((TResult)(object)boolTask.Result, input); if (result is Task doubleTask) - return ConvertToOutput((TResult)(object)doubleTask.Result, input.ActionGroup, name); + return ConvertToOutput((TResult)(object)doubleTask.Result, input); if (result is Task longTask) - return ConvertToOutput((TResult)(object)longTask.Result, input.ActionGroup, name); + return ConvertToOutput((TResult)(object)longTask.Result, input); if (result is Task decimalTask) - return ConvertToOutput((TResult)(object)decimalTask.Result, input.ActionGroup, name); + return ConvertToOutput((TResult)(object)decimalTask.Result, input); if (result is Task dateTimeTask) - return ConvertToOutput((TResult)(object)dateTimeTask.Result, input.ActionGroup, name); + return ConvertToOutput((TResult)(object)dateTimeTask.Result, input); if (result is Task guidTask) - return ConvertToOutput((TResult)(object)guidTask.Result, input.ActionGroup, name); + return ConvertToOutput((TResult)(object)guidTask.Result, input); if (result is Task objectTask) - return ConvertToOutput((TResult)objectTask.Result!, input.ActionGroup, name); + return ConvertToOutput((TResult)objectTask.Result!, input); // For regular Task with no result if (result is Task task) { task.GetAwaiter().GetResult(); - return BedrockFunctionResponse.WithText(string.Empty, input.ActionGroup, name); + return BedrockFunctionResponse.WithText( + string.Empty, + input.ActionGroup, + name, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } - return ConvertToOutput(result, input.ActionGroup, name); + return ConvertToOutput(result, input); } catch (Exception ex) { context?.Logger.LogError(ex.ToString()); var innerException = ex.InnerException ?? ex; - return BedrockFunctionResponse.WithText($"Error executing function: {innerException.Message}", - input.ActionGroup, name); + return BedrockFunctionResponse.WithText( + $"Error when invoking tool: {innerException.Message}", + input.ActionGroup, + name, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } }; @@ -535,7 +598,13 @@ private BedrockFunctionResponse HandleEvent(BedrockFunctionRequest input, ILambd { if (string.IsNullOrEmpty(input.Function)) { - return BedrockFunctionResponse.WithText("No function specified in the request", input.ActionGroup, ""); + return BedrockFunctionResponse.WithText( + "No tool specified in the request", + input.ActionGroup, + "", + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } if (_handlers.TryGetValue(input.Function, out var handler)) @@ -547,21 +616,40 @@ private BedrockFunctionResponse HandleEvent(BedrockFunctionRequest input, ILambd catch (Exception ex) { context?.Logger.LogError(ex.ToString()); - return BedrockFunctionResponse.WithText($"Error executing function: {ex.Message}", input.ActionGroup, - input.Function); + return BedrockFunctionResponse.WithText( + $"Error when invoking tool: {ex.Message}", + input.ActionGroup, + input.Function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } } - context?.Logger.LogWarning($"No handler registered for function: {input.Function}"); - return BedrockFunctionResponse.WithText($"No handler registered for function: {input.Function}", - input.ActionGroup, input.Function); + context?.Logger.LogWarning($"Tool {input.Function} has not been registered."); + return BedrockFunctionResponse.WithText( + $"Error: Tool {input.Function} has not been registered in handler", + input.ActionGroup, + input.Function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } - private BedrockFunctionResponse ConvertToOutput(T result, string actionGroup, string function) + private BedrockFunctionResponse ConvertToOutput(T result, BedrockFunctionRequest input) { + string actionGroup = input.ActionGroup; + string function = input.Function; + if (result == null) { - return BedrockFunctionResponse.WithText(string.Empty, actionGroup, function); + return BedrockFunctionResponse.WithText( + string.Empty, + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } // If result is already an BedrockFunctionResponse, ensure action group and function are set @@ -584,39 +672,78 @@ private BedrockFunctionResponse ConvertToOutput(T result, string actionGroup, // For primitive types and strings, convert to string if (result is string str) { - return BedrockFunctionResponse.WithText(str, actionGroup, function); + return BedrockFunctionResponse.WithText( + str, + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } if (result is int intVal) { - return BedrockFunctionResponse.WithText(intVal.ToString(CultureInfo.InvariantCulture), actionGroup, function); + return BedrockFunctionResponse.WithText( + intVal.ToString(CultureInfo.InvariantCulture), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } if (result is double doubleVal) { - return BedrockFunctionResponse.WithText(doubleVal.ToString(CultureInfo.InvariantCulture), actionGroup, - function); + return BedrockFunctionResponse.WithText( + doubleVal.ToString(CultureInfo.InvariantCulture), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } if (result is bool boolVal) { - return BedrockFunctionResponse.WithText(boolVal.ToString(), actionGroup, function); + return BedrockFunctionResponse.WithText( + boolVal.ToString(), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } if (result is long longVal) { - return BedrockFunctionResponse.WithText(longVal.ToString(CultureInfo.InvariantCulture), actionGroup, - function); + return BedrockFunctionResponse.WithText( + longVal.ToString(CultureInfo.InvariantCulture), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } if (result is decimal decimalVal) { - return BedrockFunctionResponse.WithText(decimalVal.ToString(CultureInfo.InvariantCulture), actionGroup, - function); + return BedrockFunctionResponse.WithText( + decimalVal.ToString(CultureInfo.InvariantCulture), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } // For any other type, use ToString() - return BedrockFunctionResponse.WithText(result.ToString() ?? string.Empty, actionGroup, function); + return BedrockFunctionResponse.WithText( + result.ToString() ?? string.Empty, + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); } } -} \ No newline at end of file +} diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs index 47ec0cfa..7887573d 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using Microsoft.Extensions.DependencyInjection; // ReSharper disable once CheckNamespace diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockFunctionResolverContext.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockFunctionResolverContext.cs new file mode 100644 index 00000000..f0f6e3fb --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockFunctionResolverContext.cs @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Text.Json.Serialization; + +// ReSharper disable once CheckNamespace +namespace AWS.Lambda.Powertools.EventHandler.Resolvers; + +[JsonSerializable(typeof(string[]))] +[JsonSerializable(typeof(int[]))] +[JsonSerializable(typeof(long[]))] +[JsonSerializable(typeof(double[]))] +[JsonSerializable(typeof(bool[]))] +[JsonSerializable(typeof(decimal[]))] +internal partial class BedrockFunctionResolverContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Agent.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Agent.cs index 5e13b2fe..05b5d20e 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Agent.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Agent.cs @@ -1,9 +1,24 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Text.Json.Serialization; namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; /// -/// Represents an agent in the Bedrock Agent function input. +/// Contains information about the name, ID, alias, and version of the agent that the action group belongs to. /// public class Agent { diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionRequest.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionRequest.cs index bd2d8395..0a9fa9ef 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionRequest.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionRequest.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Text.Json.Serialization; namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; @@ -8,55 +23,56 @@ namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Mode public class BedrockFunctionRequest { /// - /// Gets or sets the message version. + /// The version of the message that identifies the format of the event data going into the Lambda function and the expected format of the response from a Lambda function. Amazon Bedrock only supports version 1.0. /// [JsonPropertyName("messageVersion")] public string MessageVersion { get; set; } = "1.0"; /// - /// Gets or sets the function name. + /// The name of the function as defined in the function details for the action group. /// [JsonPropertyName("function")] public string Function { get; set; } = string.Empty; /// - /// Gets or sets the parameters for the function. + /// Contains a list of objects. Each object contains the name, type, and value of a parameter in the API operation, as defined in the OpenAPI schema, or in the function. /// [JsonPropertyName("parameters")] public List Parameters { get; set; } = new List(); /// - /// Gets or sets the session ID. + /// The unique identifier of the agent session. /// [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; /// - /// Gets or sets the agent information. + /// Contains information about the name, ID, alias, and version of the agent that the action group belongs to. /// [JsonPropertyName("agent")] public Agent? Agent { get; set; } /// - /// Gets or sets the action group. + /// The name of the action group. /// [JsonPropertyName("actionGroup")] public string ActionGroup { get; set; } = string.Empty; /// - /// Gets or sets the session attributes. + /// Contains session attributes and their values. These attributes are stored over a session and provide context for the agent. + /// For more information, see Session and prompt session attributes. /// [JsonPropertyName("sessionAttributes")] public Dictionary SessionAttributes { get; set; } = new Dictionary(); /// - /// Gets or sets the prompt session attributes. + /// Contains prompt session attributes and their values. These attributes are stored over a turn and provide context for the agent. /// [JsonPropertyName("promptSessionAttributes")] public Dictionary PromptSessionAttributes { get; set; } = new Dictionary(); /// - /// Gets or sets the input text. + /// The user input for the conversation turn. /// [JsonPropertyName("inputText")] public string InputText { get; set; } = string.Empty; diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionResponse.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionResponse.cs index a86df718..4d90dcba 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionResponse.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/BedrockFunctionResponse.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Text.Json.Serialization; namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; @@ -41,7 +56,13 @@ public class BedrockFunctionResponse /// /// Creates a new instance of BedrockFunctionResponse with the specified text. /// - public static BedrockFunctionResponse WithText(string text, string actionGroup = "", string function = "") + public static BedrockFunctionResponse WithText( + string? text, + string actionGroup = "", + string function = "", + Dictionary? sessionAttributes = null, + Dictionary? promptSessionAttributes = null, + Dictionary? knowledgeBasesConfiguration = null) { return new BedrockFunctionResponse { @@ -53,12 +74,13 @@ public static BedrockFunctionResponse WithText(string text, string actionGroup = { ResponseBody = new ResponseBody { - Text = new TextBody { Body = text } + Text = new TextBody { Body = text ?? string.Empty } } } }, - SessionAttributes = new Dictionary(), - PromptSessionAttributes = new Dictionary() + SessionAttributes = sessionAttributes ?? new Dictionary(), + PromptSessionAttributes = promptSessionAttributes ?? new Dictionary(), + KnowledgeBasesConfiguration = knowledgeBasesConfiguration ?? new Dictionary() }; } -} \ No newline at end of file +} diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/FunctionResponse.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/FunctionResponse.cs index e22c97d6..5366c902 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/FunctionResponse.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/FunctionResponse.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Text.Json.Serialization; // ReSharper disable InconsistentNaming #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -22,7 +37,9 @@ public class FunctionResponse /// REPROMPT – The agent passes a response string to the model to reprompt it. Applies when the function execution fails because of invalid input. /// [JsonPropertyName("responseState")] - public ResponseState ResponseState { get; set; } + [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ResponseState? ResponseState { get; set; } } /// diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Parameter.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Parameter.cs index 5e5a65ee..481eca67 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Parameter.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Parameter.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Text.Json.Serialization; // ReSharper disable once CheckNamespace diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Response.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Response.cs index 5d2e76a7..5d0720be 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Response.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/Response.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Text.Json.Serialization; namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/ResponseBody.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/ResponseBody.cs index 20bc59c2..3081af44 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/ResponseBody.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/ResponseBody.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Text.Json.Serialization; namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/TextBody.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/TextBody.cs index 8e9a41c7..f3240a87 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/TextBody.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Models/TextBody.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Text.Json.Serialization; namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs index bc8001db..e81675ca 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs @@ -1,3 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System.Globalization; // ReSharper disable once CheckNamespace diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs index 28100fe3..f98cf6c2 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs @@ -45,37 +45,6 @@ public void TestFunctionHandlerWithNoParameters() Assert.Equal("Hello, World!", result.Response.FunctionResponse.ResponseBody.Text.Body); } - [Fact] - public async Task TestFunctionHandlerWithNoParametersAsync() - { - // Arrange - var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new BedrockFunctionResponse - { - Response = new Response - { - ActionGroup = "TestGroup", - Function = "TestFunction", - FunctionResponse = new FunctionResponse - { - ResponseBody = new ResponseBody - { - Text = new TextBody { Body = "Hello, World!" } - } - } - } - }); - - var input = new BedrockFunctionRequest { Function = "TestFunction" }; - var context = new TestLambdaContext(); - - // Act - var result = await resolver.ResolveAsync(input, context); - - // Assert - Assert.Equal("Hello, World!", result.Response.FunctionResponse.ResponseBody.Text.Body); - } - [Fact] public void TestFunctionHandlerWithDescription() { @@ -240,38 +209,6 @@ public void TestFunctionHandlerWithInput() Assert.Equal("Hello, TestFunction!", result.Response.FunctionResponse.ResponseBody.Text.Body); } - [Fact] - public async Task TestFunctionHandlerWithInputAsync() - { - // Arrange - var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", - input => new BedrockFunctionResponse - { - Response = new Response - { - ActionGroup = "TestGroup", - Function = "TestFunction", - FunctionResponse = new FunctionResponse - { - ResponseBody = new ResponseBody - { - Text = new TextBody { Body = $"Hello, {input.Function}!" } - } - } - } - }); - - var input = new BedrockFunctionRequest { Function = "TestFunction" }; - var context = new TestLambdaContext(); - - // Act - var result = await resolver.ResolveAsync(input, context); - - // Assert - Assert.Equal("Hello, TestFunction!", result.Response.FunctionResponse.ResponseBody.Text.Body); - } - [Fact] public void TestFunctionHandlerNoToolMatch() { @@ -300,91 +237,10 @@ public void TestFunctionHandlerNoToolMatch() var result = resolver.Resolve(input, context); // Assert - Assert.Equal("No handler registered for function: NonExistentFunction", + Assert.Equal($"Error: Tool {input.Function} has not been registered in handler", result.Response.FunctionResponse.ResponseBody.Text.Body); } - [Fact] - public async Task TestFunctionHandlerNoToolMatchAsync() - { - // Arrange - var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new BedrockFunctionResponse - { - Response = new Response - { - ActionGroup = "TestGroup", - Function = "TestFunction", - FunctionResponse = new FunctionResponse - { - ResponseBody = new ResponseBody - { - Text = new TextBody { Body = "Hello, World!" } - } - } - } - }); - - var input = new BedrockFunctionRequest { Function = "NonExistentFunction" }; - var context = new TestLambdaContext(); - - // Act - var result = await resolver.ResolveAsync(input, context); - - // Assert - Assert.Equal("No handler registered for function: NonExistentFunction", - result.Response.FunctionResponse.ResponseBody.Text.Body); - } - - [Fact] - public void TestFunctionHandlerWithParameters() - { - // Arrange - var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("TestFunction", () => new BedrockFunctionResponse - { - Response = new Response - { - ActionGroup = "TestGroup", - Function = "TestFunction", - FunctionResponse = new FunctionResponse - { - ResponseBody = new ResponseBody - { - Text = new TextBody { Body = "Hello, World!" } - } - } - } - }); - - var input = new BedrockFunctionRequest - { - Function = "TestFunction", - Parameters = new List - { - new Parameter - { - Name = "a", - Value = "1", - Type = "Number" - }, - new Parameter - { - Name = "b", - Value = "1", - Type = "Number" - } - } - }; - var context = new TestLambdaContext(); - - // Act - var result = resolver.Resolve(input, context); - - // Assert - Assert.Equal("Hello, World!", result.Response.FunctionResponse.ResponseBody.Text.Body); - } - [Fact] public void TestFunctionHandlerWithEvent() { @@ -491,56 +347,6 @@ public void TestFunctionHandlerWithEventAndServices() Assert.Equal("Forecast for Lisbon for 1 days", result.Response.FunctionResponse.ResponseBody.Text.Body); } - [Fact] - public void TestFunctionHandlerWithEventTypes() - { - // Arrange - var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool( - name: "GetCustomForecast", - description: "Get detailed forecast for a location", - handler: (string location, int days, ILambdaContext ctx) => - { - ctx.Logger.LogLine($"Getting forecast for {location}"); - return $"{days}-day forecast for {location}"; - } - ); - - resolver.Tool( - name: "Greet", - description: "Greet a user", - handler: (string name) => { return $"Hello {name}"; } - ); - - var input = new BedrockFunctionRequest - { - Function = "GetCustomForecast", - Parameters = new List - { - new Parameter - { - Name = "location", - Value = "Lisbon", - Type = "String" - }, - new Parameter - { - Name = "days", - Value = "1", - Type = "Number" - } - } - }; - - var context = new TestLambdaContext(); - - // Act - var result = resolver.Resolve(input, context); - - // Assert - Assert.Equal("1-day forecast for Lisbon", result.Response.FunctionResponse.ResponseBody.Text.Body); - } - [Fact] public void TestFunctionHandlerWithBooleanParameter() { @@ -765,43 +571,6 @@ public void TestFunctionHandlerWithDecimalParameter() Assert.Contains("35.99", result.Response.FunctionResponse.ResponseBody.Text.Body); } - [Fact] - public void TestFunctionHandlerWithArrayParameter() - { - // Arrange - var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool( - name: "ArrayTest", - description: "Test with array parameter", - handler: (string text) => - { - // In a real implementation, you'd parse the array from the string - // BedrockFunctionRequest doesn't directly support array types - return $"Received: {text}"; - } - ); - - var input = new BedrockFunctionRequest - { - Function = "ArrayTest", - Parameters = new List - { - new Parameter - { - Name = "text", - Value = "[\"item1\",\"item2\"]", // Array as JSON string - Type = "Array" - } - } - }; - - // Act - var result = resolver.Resolve(input); - - // Assert - Assert.Equal("Received: [\"item1\",\"item2\"]", result.Response.FunctionResponse.ResponseBody.Text.Body); - } - [Fact] public void TestFunctionHandlerWithStringArrayParameter() { @@ -850,49 +619,57 @@ public void TestFunctionHandlerWithStringArrayParameter() } [Fact] - public void TestFunctionHandlerWithStringArrayParameterManualParse() + public void TestFunctionHandlerWithExceptionInHandler() { // Arrange var resolver = new BedrockAgentFunctionResolver(); resolver.Tool( - name: "ProcessWorkout", - description: "Process workout exercises", - handler: (BedrockFunctionRequest input) => + name: "ThrowingFunction", + description: "Function that throws exception", + handler: () => { - // Manual array parsing since the resolver doesn't natively support arrays - var exercisesJson = input.Parameters.FirstOrDefault(p => p.Name == "exercises")?.Value ?? "[]"; + throw new InvalidOperationException("Test error"); + return "This will not run:"; + } + ); - // Parse JSON array - var exercises = JsonSerializer.Deserialize(exercisesJson); + var input = new BedrockFunctionRequest { Function = "ThrowingFunction" }; - // Process the array items - var result = new StringBuilder(); - result.AppendLine("Your workout plan:"); + // Act + var result = resolver.Resolve(input); - if (exercises != null) - { - for (int i = 0; i < exercises.Length; i++) - { - result.AppendLine($" {i + 1}. {exercises[i]}"); - } - } + // Assert + Assert.Contains("Error when invoking tool: Test error", result.Response.FunctionResponse.ResponseBody.Text.Body); + } - return result.ToString(); - } + [Fact] + public void TestSessionAttributesPreservation() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool( + name: "SessionTest", + description: "Test session attributes preservation", + handler: (string message) => message ); - var input = new BedrockFunctionRequest - { - Function = "ProcessWorkout", + var input = new BedrockFunctionRequest + { + Function = "SessionTest", + ActionGroup = "TestGroup", Parameters = new List { - new Parameter - { - Name = "exercises", - Value = - "[\"Squats, 3 sets of 10 reps\",\"Push-ups, 3 sets of 10 reps\",\"Plank, 3 sets of 30 seconds\"]", - Type = "String" // The type is still String even though it contains JSON - } + new Parameter { Name = "message", Value = "Hello", Type = "String" } + }, + SessionAttributes = new Dictionary + { + { "userId", "12345" }, + { "preferredLanguage", "en-US" } + }, + PromptSessionAttributes = new Dictionary + { + { "context", "customer_support" }, + { "previousQuestion", "How do I reset my password?" } } }; @@ -900,57 +677,167 @@ public void TestFunctionHandlerWithStringArrayParameterManualParse() var result = resolver.Resolve(input); // Assert - Assert.Contains("Your workout plan:", result.Response.FunctionResponse.ResponseBody.Text.Body); - Assert.Contains("1. Squats, 3 sets of 10 reps", result.Response.FunctionResponse.ResponseBody.Text.Body); - Assert.Contains("2. Push-ups, 3 sets of 10 reps", result.Response.FunctionResponse.ResponseBody.Text.Body); - Assert.Contains("3. Plank, 3 sets of 30 seconds", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("Hello", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal(2, result.SessionAttributes.Count); + Assert.Equal("12345", result.SessionAttributes["userId"]); + Assert.Equal("en-US", result.SessionAttributes["preferredLanguage"]); + Assert.Equal(2, result.PromptSessionAttributes.Count); + Assert.Equal("customer_support", result.PromptSessionAttributes["context"]); + Assert.Equal("How do I reset my password?", result.PromptSessionAttributes["previousQuestion"]); } [Fact] - public async Task TestPayload2() + public void TestSessionAttributesPreservationWithErrorHandling() { // Arrange var resolver = new BedrockAgentFunctionResolver(); - resolver.Tool("get_weather_city", "Get weather for a specific city", - async (string city, ILambdaContext context) => { return await Task.FromResult(city); }); + resolver.Tool( + name: "ErrorTest", + description: "Test session attributes preservation with error", + handler: () => { throw new Exception("Test error"); return "This will not run"; } + ); - var input = JsonSerializer.Deserialize( - File.ReadAllText("bedrockFunctionEvent2.json"), - new JsonSerializerOptions + var input = new BedrockFunctionRequest + { + Function = "ErrorTest", + ActionGroup = "TestGroup", + SessionAttributes = new Dictionary + { + { "userId", "12345" }, + { "session", "active" } + }, + PromptSessionAttributes = new Dictionary { - PropertyNameCaseInsensitive = true, - Converters = { new JsonStringEnumConverter() } - })!; + { "lastAction", "login" } + } + }; // Act - var result = await resolver.ResolveAsync(input); + var result = resolver.Resolve(input); // Assert - Assert.Equal("Lisbon", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Contains("Error when invoking tool: Test error", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal(2, result.SessionAttributes.Count); + Assert.Equal("12345", result.SessionAttributes["userId"]); + Assert.Equal("active", result.SessionAttributes["session"]); + Assert.Equal(1, result.PromptSessionAttributes?.Count); + Assert.Equal("login", result.PromptSessionAttributes?["lastAction"]); } [Fact] - public void TestFunctionHandlerWithExceptionInHandler() + public void TestSessionAttributesPreservationWithNoToolMatch() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + + var input = new BedrockFunctionRequest + { + Function = "NonExistentTool", + SessionAttributes = new Dictionary + { + { "preferredTheme", "dark" } + }, + PromptSessionAttributes = new Dictionary + { + { "lastVisited", "homepage" } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains($"Error: Tool {input.Function} has not been registered in handler", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal(1, result.SessionAttributes?.Count); + Assert.Equal("dark", result.SessionAttributes?["preferredTheme"]); + Assert.Equal(1, result.PromptSessionAttributes?.Count); + Assert.Equal("homepage", result.PromptSessionAttributes?["lastVisited"]); + } + + [Fact] + public void TestSReturningNull() { // Arrange var resolver = new BedrockAgentFunctionResolver(); resolver.Tool( - name: "ThrowingFunction", - description: "Function that throws exception", + name: "NullTest", + description: "Test session attributes preservation with error", handler: () => { - throw new InvalidOperationException("Test error"); - return "This will not run:"; + string test = null!; + return test; } ); - - var input = new BedrockFunctionRequest { Function = "ThrowingFunction" }; + + var input = new BedrockFunctionRequest + { + Function = "NullTest", + }; // Act var result = resolver.Resolve(input); // Assert - Assert.Contains("Error executing function", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void TestMaximumToolLimit() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + + // Register 5 tools (the maximum) + for (int i = 1; i <= 5; i++) + { + var toolName = $"Tool{i}"; + var response = $"Response from {toolName}"; + resolver.Tool(toolName, () => response); + + // Verify each tool works as it's registered + var testInput = new BedrockFunctionRequest { Function = toolName }; + var testResult = resolver.Resolve(testInput); + Assert.Contains(response, testResult.Response.FunctionResponse.ResponseBody.Text.Body); + } + + // Try to register a 6th tool that should not be registered + resolver.Tool("Tool6", () => "This should not be registered"); + + // Verify the 6th tool doesn't work + var input6 = new BedrockFunctionRequest { Function = "Tool6" }; + var result6 = resolver.Resolve(input6); + + // 6th tool should not be registered + Assert.Contains("has not been registered", result6.Response.FunctionResponse.ResponseBody.Text.Body); + + // Double-check that the original 5 tools still work + for (int i = 1; i <= 5; i++) + { + var toolName = $"Tool{i}"; + var input = new BedrockFunctionRequest { Function = toolName }; + var result = resolver.Resolve(input); + Assert.Contains($"Response from {toolName}", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + } + + [Fact] + public void TestToolOverrideWithWarning() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + + // Register a tool + resolver.Tool("Calculator", () => "Original Calculator"); + + // Register same tool again with different implementation + resolver.Tool("Calculator", () => "New Calculator"); + + // Verify the tool was overridden + var input = new BedrockFunctionRequest { Function = "Calculator" }; + var result = resolver.Resolve(input); + + // The second registration should have overwritten the first + Assert.Equal("New Calculator", result.Response.FunctionResponse.ResponseBody.Text.Body); } } @@ -965,4 +852,4 @@ public async Task DoSomething(string location, int days) { return await Task.FromResult($"Forecast for {location} for {days} days"); } -} \ No newline at end of file +} From 6ce9b7bf77f30e741bccc95b26961564fb50ed95 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 27 May 2025 22:36:36 +0100 Subject: [PATCH 34/52] refactor BedrockAgentFunctionResolver to improve parameter type handling and streamline task result processing --- .../BedrockAgentFunctionResolver.cs | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs index d8e08bb0..c15bc13e 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs @@ -45,7 +45,7 @@ private readonly Dictionary> _handlers = new(); - private static readonly HashSet _bedrockParameterTypes = new() + private static readonly HashSet BedrockParameterTypes = new() { typeof(string), typeof(int), @@ -64,8 +64,8 @@ private readonly }; private static bool IsBedrockParameter(Type type) => - _bedrockParameterTypes.Contains(type) || type.IsEnum || - (type.IsArray && _bedrockParameterTypes.Contains(type.GetElementType()!)); + BedrockParameterTypes.Contains(type) || type.IsEnum || + (type.IsArray && BedrockParameterTypes.Contains(type.GetElementType()!)); /// /// Checks if another tool can be registered, and logs a warning if the maximum limit is reached @@ -507,22 +507,19 @@ public BedrockAgentFunctionResolver Tool( if (result is Task guidTask) return ConvertToOutput((TResult)(object)guidTask.Result, input); if (result is Task objectTask) - return ConvertToOutput((TResult)objectTask.Result!, input); + return ConvertToOutput((TResult)objectTask.Result, input); // For regular Task with no result - if (result is Task task) - { - task.GetAwaiter().GetResult(); - return BedrockFunctionResponse.WithText( - string.Empty, - input.ActionGroup, - name, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } + if (result is not Task task) return ConvertToOutput(result, input); + task.GetAwaiter().GetResult(); + return BedrockFunctionResponse.WithText( + string.Empty, + input.ActionGroup, + name, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); - return ConvertToOutput(result, input); } catch (Exception ex) { From de7488600bce119db2242afa67b6e76f817246ce Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 28 May 2025 09:45:48 +0100 Subject: [PATCH 35/52] refactor BedrockAgentFunctionResolver to remove tool registration limit and add attribute-based tool registration support --- .../BedrockAgentFunctionResolver.cs | 8 -- .../BedrockAgentFunctionResolverExtensions.cs | 98 ++++++++++++---- .../BedrockFunctionToolAttribute.cs | 60 ++++++++++ .../DiBedrockAgentFunctionResolver.cs | 21 ++++ .../BedrockAgentFunctionResolverTests.cs | 105 +++++++++++------- 5 files changed, 221 insertions(+), 71 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockFunctionToolAttribute.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/DiBedrockAgentFunctionResolver.cs diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs index c15bc13e..133db96c 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs @@ -39,8 +39,6 @@ namespace AWS.Lambda.Powertools.EventHandler.Resolvers /// public class BedrockAgentFunctionResolver { - private const int MaxTools = 5; - private readonly Dictionary> _handlers = new(); @@ -75,12 +73,6 @@ private static bool IsBedrockParameter(Type type) => /// True if the tool can be registered, false if the maximum limit is reached private bool CanRegisterTool(string name) { - if (_handlers.Count >= MaxTools && !_handlers.ContainsKey(name)) - { - Console.WriteLine($"WARNING: Maximum number of tools ({MaxTools}) reached. Tool '{name}' will not be registered."); - return false; - } - if (_handlers.ContainsKey(name)) { Console.WriteLine($"WARNING: Tool {name} already registered. Overwriting with new definition."); diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs index 7887573d..bd2c8cbc 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs @@ -13,31 +13,14 @@ * permissions and limitations under the License. */ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Reflection; using Microsoft.Extensions.DependencyInjection; // ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler.Resolvers { - /// - /// Extended Bedrock Agent Function Resolver with dependency injection support. - /// - internal class DiBedrockAgentFunctionResolver : BedrockAgentFunctionResolver - { - /// - /// Gets the service provider used for dependency injection. - /// - public IServiceProvider ServiceProvider { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The service provider for dependency injection. - public DiBedrockAgentFunctionResolver(IServiceProvider serviceProvider) - { - ServiceProvider = serviceProvider; - } - } - /// /// Extension methods for Bedrock Agent Function Resolver. /// @@ -48,11 +31,86 @@ public static class BedrockResolverExtensions /// /// The service collection to add the resolver to. /// The updated service collection. + /// + /// + /// public void ConfigureServices(IServiceCollection services) + /// { + /// services.AddBedrockResolver(); + /// + /// // Now you can inject BedrockAgentFunctionResolver into your services + /// } + /// + /// public static IServiceCollection AddBedrockResolver(this IServiceCollection services) { services.AddSingleton(sp => new DiBedrockAgentFunctionResolver(sp)); return services; } + + /// + /// Registers tools from a type marked with BedrockFunctionTypeAttribute. + /// + /// The type containing tool methods marked with BedrockFunctionToolAttribute + /// The resolver to register tools with + /// The resolver for method chaining + /// + /// + /// // Define your tool class + /// [BedrockFunctionType] + /// public class WeatherTools + /// { + /// [BedrockFunctionTool(Name = "GetWeather", Description = "Gets weather forecast")] + /// public static string GetWeather(string location, int days) + /// { + /// return $"Weather forecast for {location} for the next {days} days"; + /// } + /// } + /// + /// // Register the tools + /// var resolver = new BedrockAgentFunctionResolver(); + /// resolver.RegisterTool<WeatherTools>(); + /// + /// + public static BedrockAgentFunctionResolver RegisterTool<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>( + this BedrockAgentFunctionResolver resolver) + where T : class + { + var type = typeof(T); + + // Check if class has the BedrockFunctionType attribute + if (!type.IsDefined(typeof(BedrockFunctionTypeAttribute), false)) + return resolver; + + // Look at all static methods with the tool attribute + foreach (var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public)) + { + var attr = method.GetCustomAttribute(); + if (attr == null) continue; + + string toolName = attr.Name ?? method.Name; + string description = attr.Description ?? + string.Empty; + + // Create delegate from the static method + var del = Delegate.CreateDelegate( + GetDelegateType(method), + method); + + // Call the Tool method directly instead of using reflection + resolver.Tool(toolName, description, del); + } + + return resolver; + } + + private static Type GetDelegateType(MethodInfo method) + { + var parameters = method.GetParameters(); + var parameterTypes = parameters.Select(p => p.ParameterType).ToList(); + parameterTypes.Add(method.ReturnType); + + return Expression.GetDelegateType(parameterTypes.ToArray()); + } } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockFunctionToolAttribute.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockFunctionToolAttribute.cs new file mode 100644 index 00000000..562e7804 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockFunctionToolAttribute.cs @@ -0,0 +1,60 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +// ReSharper disable once CheckNamespace +namespace AWS.Lambda.Powertools.EventHandler.Resolvers; + +/// +/// Marks a method as a Bedrock Agent function tool. +/// +/// +/// +/// [BedrockFunctionTool(Name = "GetWeather", Description = "Gets the weather for a location")] +/// public static string GetWeather(string location, int days) +/// { +/// return $"Weather forecast for {location} for the next {days} days"; +/// } +/// +/// +[AttributeUsage(AttributeTargets.Method)] +public class BedrockFunctionToolAttribute : Attribute +{ + /// + /// The name of the tool. If not specified, the method name will be used. + /// + public string? Name { get; set; } + + /// + /// The description of the tool. Used to provide context about the tool's functionality. + /// + public string? Description { get; set; } +} + +/// +/// Marks a class as containing Bedrock Agent function tools. +/// +/// +/// +/// [BedrockFunctionType] +/// public class WeatherTools +/// { +/// // Methods that can be registered as tools +/// } +/// +/// +[AttributeUsage(AttributeTargets.Class)] +public class BedrockFunctionTypeAttribute : Attribute +{ +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/DiBedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/DiBedrockAgentFunctionResolver.cs new file mode 100644 index 00000000..0d893623 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/DiBedrockAgentFunctionResolver.cs @@ -0,0 +1,21 @@ +namespace AWS.Lambda.Powertools.EventHandler.Resolvers; + +/// +/// Extended Bedrock Agent Function Resolver with dependency injection support. +/// +internal class DiBedrockAgentFunctionResolver : BedrockAgentFunctionResolver +{ + /// + /// Gets the service provider used for dependency injection. + /// + public IServiceProvider ServiceProvider { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The service provider for dependency injection. + public DiBedrockAgentFunctionResolver(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs index f98cf6c2..50b28fe6 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs @@ -1,7 +1,5 @@ using System.Globalization; using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; @@ -781,45 +779,6 @@ public void TestSReturningNull() Assert.Equal("", result.Response.FunctionResponse.ResponseBody.Text.Body); } - [Fact] - public void TestMaximumToolLimit() - { - // Arrange - var resolver = new BedrockAgentFunctionResolver(); - - // Register 5 tools (the maximum) - for (int i = 1; i <= 5; i++) - { - var toolName = $"Tool{i}"; - var response = $"Response from {toolName}"; - resolver.Tool(toolName, () => response); - - // Verify each tool works as it's registered - var testInput = new BedrockFunctionRequest { Function = toolName }; - var testResult = resolver.Resolve(testInput); - Assert.Contains(response, testResult.Response.FunctionResponse.ResponseBody.Text.Body); - } - - // Try to register a 6th tool that should not be registered - resolver.Tool("Tool6", () => "This should not be registered"); - - // Verify the 6th tool doesn't work - var input6 = new BedrockFunctionRequest { Function = "Tool6" }; - var result6 = resolver.Resolve(input6); - - // 6th tool should not be registered - Assert.Contains("has not been registered", result6.Response.FunctionResponse.ResponseBody.Text.Body); - - // Double-check that the original 5 tools still work - for (int i = 1; i <= 5; i++) - { - var toolName = $"Tool{i}"; - var input = new BedrockFunctionRequest { Function = toolName }; - var result = resolver.Resolve(input); - Assert.Contains($"Response from {toolName}", result.Response.FunctionResponse.ResponseBody.Text.Body); - } - } - [Fact] public void TestToolOverrideWithWarning() { @@ -839,14 +798,74 @@ public void TestToolOverrideWithWarning() // The second registration should have overwritten the first Assert.Equal("New Calculator", result.Response.FunctionResponse.ResponseBody.Text.Body); } + + [Fact] + public void TestAttributeBasedToolRegistration() + { + // Arrange + + var services = new ServiceCollection(); + services.AddSingleton(new MyImplementation()); + services.AddBedrockResolver(); + + var serviceProvider = services.BuildServiceProvider(); + var resolver = serviceProvider.GetRequiredService() + .RegisterTool(); + + // Create test input for echo function + var echoInput = new BedrockFunctionRequest + { + Function = "Echo", + Parameters = new List + { + new Parameter { Name = "message", Value = "Hello world", Type = "String" } + } + }; + + // Create test input for calculate function + var calcInput = new BedrockFunctionRequest + { + Function = "Calculate", + Parameters = new List + { + new Parameter { Name = "x", Value = "5", Type = "Number" }, + new Parameter { Name = "y", Value = "3", Type = "Number" } + } + }; + + // Act + var echoResult = resolver.Resolve(echoInput); + var calcResult = resolver.Resolve(calcInput); + + // Assert + Assert.Equal("You asked: Forecast for Lisbon for 1 days", echoResult.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("Result: 8", calcResult.Response.FunctionResponse.ResponseBody.Text.Body); + } + + // Example tool class using attributes + [BedrockFunctionType] + public class AttributeBasedTool + { + [BedrockFunctionTool(Name = "Echo", Description = "Echoes back the input message")] + public static string EchoMessage(string message, IMyInterface myInterface, ILambdaContext context) + { + return $"You asked: {myInterface.DoSomething("Lisbon", 1).Result}"; + } + + [BedrockFunctionTool(Name = "Calculate", Description = "Adds two numbers together")] + public static string Calculate(int x, int y) + { + return $"Result: {x + y}"; + } + } } -internal interface IMyInterface +public interface IMyInterface { Task DoSomething(string location, int days); } -internal class MyImplementation : IMyInterface +public class MyImplementation : IMyInterface { public async Task DoSomething(string location, int days) { From 7868e408f0994b77a2f06c047adaaebc3a3fc5c9 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 28 May 2025 20:42:44 +0100 Subject: [PATCH 36/52] add ASP.NET Core integration for Bedrock Agent Function Resolver with fluent API for function registration and automatic request processing --- libraries/AWS.Lambda.Powertools.sln | 15 ++ ...ers.BedrockAgentFunction.AspNetCore.csproj | 26 +++ .../BedrockFunctionRegistration.cs | 56 ++++++ .../BedrockMinimalApiExtensions.cs | 173 ++++++++++++++++++ .../Readme.md | 115 ++++++++++++ .../Readme.md | 65 +++++-- 6 files changed, 434 insertions(+), 16 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore.csproj create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/BedrockFunctionRegistration.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/BedrockMinimalApiExtensions.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/Readme.md diff --git a/libraries/AWS.Lambda.Powertools.sln b/libraries/AWS.Lambda.Powertools.sln index 056b3801..c3056d14 100644 --- a/libraries/AWS.Lambda.Powertools.sln +++ b/libraries/AWS.Lambda.Powertools.sln @@ -111,6 +111,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Event EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction", "src\AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction\AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj", "{281F7EB5-ACE5-458F-BC88-46A8899DF3BA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore", "src\AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore\AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore.csproj", "{8A22F22E-D10A-4897-A89A-DC76C267F6BB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -604,6 +606,18 @@ Global {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|x64.Build.0 = Release|Any CPU {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|x86.ActiveCfg = Release|Any CPU {281F7EB5-ACE5-458F-BC88-46A8899DF3BA}.Release|x86.Build.0 = Release|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Debug|x64.Build.0 = Debug|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Debug|x86.Build.0 = Debug|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Release|Any CPU.Build.0 = Release|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Release|x64.ActiveCfg = Release|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Release|x64.Build.0 = Release|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Release|x86.ActiveCfg = Release|Any CPU + {8A22F22E-D10A-4897-A89A-DC76C267F6BB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution @@ -656,5 +670,6 @@ Global {61374D8E-F77C-4A31-AE07-35DAF1847369} = {1CFF5568-8486-475F-81F6-06105C437528} {F4B8D5AF-D3CA-4910-A14D-E5BAEF0FD1DE} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5} {281F7EB5-ACE5-458F-BC88-46A8899DF3BA} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5} + {8A22F22E-D10A-4897-A89A-DC76C267F6BB} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5} EndGlobalSection EndGlobal diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore.csproj b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore.csproj new file mode 100644 index 00000000..5e5c6666 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore.csproj @@ -0,0 +1,26 @@ + + + + + AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore + Powertools for AWS Lambda (.NET) - Event Handler Bedrock Agent Function Resolver AspNetCore package. + AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore + AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore + net8.0 + false + enable + enable + + + + + + + + + + + + + + diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/BedrockFunctionRegistration.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/BedrockFunctionRegistration.cs new file mode 100644 index 00000000..9004a49d --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/BedrockFunctionRegistration.cs @@ -0,0 +1,56 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore; + +/// +/// Helper class for function registration with fluent API pattern. +/// +internal class BedrockFunctionRegistration +{ + private readonly BedrockAgentFunctionResolver _resolver; + + /// + /// Initializes a new instance of the class. + /// + /// The Bedrock agent function resolver. + public BedrockFunctionRegistration(BedrockAgentFunctionResolver resolver) + { + _resolver = resolver; + } + + /// + /// Adds a function to the Bedrock resolver. + /// + /// The name of the function. + /// The delegate handler. + /// Optional description of the function. + /// The function registration instance for method chaining. + /// + /// + /// app.MapBedrockFunction("GetWeather", (string city, int month) => + /// $"Weather forecast for {city} in month {month}: Warm and sunny"); + /// + /// app.MapBedrockFunction("Calculate", (int x, int y) => + /// $"Result: {x + y}"); + /// ); + /// + /// + public BedrockFunctionRegistration Add(string name, Delegate handler, string description = "") + { + _resolver.Tool(name, description, handler); + return this; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/BedrockMinimalApiExtensions.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/BedrockMinimalApiExtensions.cs new file mode 100644 index 00000000..9cbe0ae1 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/BedrockMinimalApiExtensions.cs @@ -0,0 +1,173 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore; + +// Source generation for JSON serialization +[JsonSerializable(typeof(BedrockFunctionRequest))] +internal partial class BedrockJsonContext : JsonSerializerContext +{ +} + +/// +/// Extension methods for registering Bedrock Agent Functions in ASP.NET Core Minimal API. +/// +public static class BedrockMinimalApiExtensions +{ + // Static flag to track if handler is mapped (thread-safe with volatile) + private static volatile bool _bedrockRequestHandlerMapped; + + // JSON options with case insensitivity + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + /// + /// Maps an individual Bedrock Agent function that will be called directly from the root endpoint. + /// The function name is extracted from the incoming request payload. + /// + /// The web application to configure. + /// The name of the function to register. + /// The delegate handler that implements the function. + /// Optional description of the function. + /// The web application instance. + /// + /// + /// // Register individual functions + /// app.MapBedrockFunction("GetWeather", (string city, int month) => + /// $"Weather forecast for {city} in month {month}: Warm and sunny"); + /// + /// app.MapBedrockFunction("Calculate", (int x, int y) => + /// $"Result: {x + y}"); + /// + /// + public static WebApplication MapBedrockFunction( + this WebApplication app, + string functionName, + Delegate handler, + string description = "") + { + // Get or create the resolver from services + var resolver = app.Services.GetService() + ?? new BedrockAgentFunctionResolver(); + + // Register the function with the resolver + resolver.Tool(functionName, description, handler); + + // Ensure we have a global handler for Bedrock requests + EnsureBedrockRequestHandler(app, resolver); + + return app; + } + + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", + Justification = "The handler implementation is controlled and AOT-compatible")] + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The handler implementation is controlled and trim-compatible")] + private static void EnsureBedrockRequestHandler(WebApplication app, BedrockAgentFunctionResolver resolver) + { + // Check if we've already mapped the handler (we only need to do this once) + if (_bedrockRequestHandlerMapped) + return; + + // Map the root endpoint to handle all Bedrock Agent Function requests + app.MapPost("/", [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Handler is AOT-friendly")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Handler is trim-friendly")] + async (HttpContext context) => + { + try + { + // Read the request body + string requestBody; + using (var reader = new StreamReader(context.Request.Body)) + { + requestBody = await reader.ReadToEndAsync(); + } + + // Use source-generated serialization for the request + var bedrockRequest = JsonSerializer.Deserialize(requestBody, + BedrockJsonContext.Default.BedrockFunctionRequest); + + if (bedrockRequest == null) + return Results.BadRequest("Invalid request format"); + + // Process the request through the resolver + var result = await resolver.ResolveAsync(bedrockRequest); + + // For the response, use the standard serializer with suppressed warnings + // This is more compatible with different response types + context.Response.ContentType = "application/json"; + await context.Response.WriteAsJsonAsync(result, JsonOptions); + return Results.Empty; + } + catch (Exception ex) + { + return Results.Problem($"Error processing Bedrock Agent request: {ex.Message}"); + } + }); + + // Mark that we've set up the handler + _bedrockRequestHandlerMapped = true; + } + + /// + /// Registers all methods from a class marked with BedrockFunctionTypeAttribute. + /// + /// The type containing tool methods marked with BedrockFunctionToolAttribute + /// The web application to configure. + /// The web application instance. + /// + /// + /// // Define your tool class + /// [BedrockFunctionType] + /// public class WeatherTools + /// { + /// [BedrockFunctionTool(Name = "GetWeather", Description = "Gets weather forecast")] + /// public static string GetWeather(string location, int days) + /// { + /// return $"Weather forecast for {location} for the next {days} days"; + /// } + /// } + /// + /// // Register all tools from the class + /// app.MapBedrockToolClass<WeatherTools>(); + /// + /// + public static WebApplication MapBedrockToolType<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>( + this WebApplication app) + where T : class + { + // Get or create the resolver from services + var resolver = app.Services.GetService() + ?? new BedrockAgentFunctionResolver(); + + // Register the tool class + resolver.RegisterTool(); + + // Ensure we have a global handler for Bedrock requests + EnsureBedrockRequestHandler(app, resolver); + + return app; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/Readme.md b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/Readme.md new file mode 100644 index 00000000..8cc31365 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore/Readme.md @@ -0,0 +1,115 @@ +# Experimental work in progress, not yet released + +# AWS Lambda Powertools for .NET - Bedrock Agent Function Resolver for ASP.NET Core + +## Overview +This library provides ASP.NET Core integration for the AWS Lambda Powertools Bedrock Agent Function Resolver. It enables you to easily expose Bedrock Agent functions as endpoints in your ASP.NET Core applications using a simple, fluent API. + +## Features + +- **Minimal API Integration**: Register Bedrock Agent functions using familiar ASP.NET Core Minimal API patterns +- **AOT Compatibility**: Full support for .NET 8 AOT compilation through source generation +- **Simple Function Registration**: Register functions with a fluent API +- **Automatic Request Processing**: Automatic parsing of Bedrock Agent requests and formatting of responses +- **Error Handling**: Built-in error handling for Bedrock Agent function requests + +## Installation + +Install the package via NuGet: + +```bash +dotnet add package AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore +``` + +## Basic Usage + +Here's how to register Bedrock Agent functions in your ASP.NET Core application: + +```csharp +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +// Register individual functions +app.MapBedrockFunction("GetWeather", (string city, int month) => + $"Weather forecast for {city} in month {month}: Warm and sunny"); + +app.MapBedrockFunction("Calculate", (int x, int y) => + $"Result: {x + y}"); + +app.Run(); +``` + +When Amazon Bedrock Agent sends a request to your application, the appropriate function will be invoked with the extracted parameters, and the response will be formatted correctly for the agent. + +## Using with Dependency Injection + +Register the Bedrock resolver with dependency injection for more advanced scenarios: + +```csharp +using AWS.Lambda.Powertools.EventHandler.Resolvers; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); + +// Register the resolver and any other services +builder.Services.AddBedrockResolver(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +// Register functions that use injected services +app.MapBedrockFunction("GetWeatherForecast", + (string city, IWeatherService weatherService) => + weatherService.GetForecast(city), + "Gets weather forecast for a city"); + +app.Run(); +``` + +## Advanced Usage + +### Function Documentation + +Add descriptions to your functions for better documentation: + +```csharp +app.MapBedrockFunction("GetWeather", + (string city, int month) => $"Weather forecast for {city} in month {month}: Warm and sunny", + "Gets weather forecast for a specific city and month"); +``` + +### Working with Tool Classes + +Use the `MapBedrockToolClass()` method to register all functions from a class directly: + +```csharp +[BedrockFunctionType] +public class WeatherTools +{ + [BedrockFunctionTool(Name = "GetWeather", Description = "Gets weather forecast")] + public static string GetWeather(string location, int days) + { + return $"Weather forecast for {location} for the next {days} days"; + } +} + +// In Program.cs - directly register the tool class +app.MapBedrockToolClass(); +``` + +## How It Works + +1. When you call `MapBedrockFunction`, the function is registered with the resolver +2. An HTTP endpoint is set up at the root path (/) to handle incoming Bedrock Agent requests +3. When a request arrives, the library: + - Deserializes the JSON payload + - Extracts the function name and parameters + - Invokes the matching function with the appropriate parameters + - Serializes the result and returns it as a response + +## Requirements + +- .NET 8.0 or later +- ASP.NET Core 8.0 or later \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md index decd8abb..d1017d76 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md @@ -204,24 +204,61 @@ resolver.Tool( ## Supported Parameter Types - `string` -- `int` / `long` -- `double` / `decimal` +- `int` +- `number` - `bool` -- `DateTime` -- `Guid` - `enum` types - `ILambdaContext` (for accessing Lambda context) - `ActionGroupInvocationInput` (for accessing raw request) - Any service registered in dependency injection -## Benefits -- **Reduced Boilerplate**: Eliminate repetitive code for parsing requests and formatting responses -- **Type Safety**: Strong typing for parameters and return values -- **Simplified Development**: Focus on business logic instead of request/response handling -- **Reusable Components**: Build a library of tool functions that can be shared across agents -- **Easy Testing**: Functions can be easily unit tested in isolation -- **Flexible Integration**: Works seamlessly with AWS Lambda and Bedrock Agents +## 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 + +```csharp +// 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: + +```csharp + +var services = new ServiceCollection(); +services.AddSingleton(); +services.AddBedrockResolver(); // Extension method to register the resolver + +var serviceProvider = services.BuildServiceProvider(); +var resolver = serviceProvider.GetRequiredService() + .RegisterTool(); // Register tools from the class during service registration + +``` ## Complete Example with Dependency Injection @@ -297,8 +334,4 @@ namespace MyBedrockAgent } } } -``` - -## Learn More - -For more information about Amazon Bedrock Agents and function integration, see the [Amazon Bedrock documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-tools.html). +``` \ No newline at end of file From 87c6423348bc7f488322697e0dd567b34d350a1f Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 28 May 2025 20:44:18 +0100 Subject: [PATCH 37/52] bump BedrockAgentFunctionResolver version to 1.0.0-alpha.1 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 759ed8a3..cb5c98bc 100644 --- a/version.json +++ b/version.json @@ -10,6 +10,6 @@ "Idempotency": "1.3.0", "BatchProcessing": "1.2.1", "EventHandler": "1.0.0", - "BedrockAgentFunctionResolver": "1.0.0" + "BedrockAgentFunctionResolver": "1.0.0-alpha.1", } } From 553f2f62373f9d2ccf961cd1b8a1df909e03cd14 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 May 2025 09:32:30 +0100 Subject: [PATCH 38/52] refactor BedrockAgentFunctionResolver to enhance parameter mapping and result processing with dedicated helper classes --- .../BedrockAgentFunctionResolver.cs | 479 ++---------------- .../Helpers/ParameterMapper.cs | 150 ++++++ .../Helpers/ParameterTypeValidator.cs | 50 ++ .../Helpers/ResultConverter.cs | 238 +++++++++ 4 files changed, 493 insertions(+), 424 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterTypeValidator.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs index 133db96c..a4ece182 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs @@ -17,6 +17,7 @@ using System.Text.Json; using Amazon.Lambda.Core; using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers; // ReSharper disable once CheckNamespace namespace AWS.Lambda.Powertools.EventHandler.Resolvers @@ -43,27 +44,9 @@ private readonly Dictionary> _handlers = new(); - private static readonly HashSet BedrockParameterTypes = new() - { - typeof(string), - typeof(int), - typeof(long), - typeof(double), - typeof(bool), - typeof(decimal), - typeof(DateTime), - typeof(Guid), - typeof(string[]), - typeof(int[]), - typeof(long[]), - typeof(double[]), - typeof(bool[]), - typeof(decimal[]) - }; - - private static bool IsBedrockParameter(Type type) => - BedrockParameterTypes.Contains(type) || type.IsEnum || - (type.IsArray && BedrockParameterTypes.Contains(type.GetElementType()!)); + private readonly ParameterTypeValidator _parameterValidator = new(); + private readonly ResultConverter _resultConverter = new(); + private readonly ParameterMapper _parameterMapper = new(); /// /// Checks if another tool can be registered, and logs a warning if the maximum limit is reached @@ -88,26 +71,12 @@ private bool CanRegisterTool(string name) /// The handler function that accepts input and context and returns output /// Optional description of the tool function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// resolver.Tool( - /// "GetWeatherDetails", - /// (BedrockFunctionRequest input, ILambdaContext context) => { - /// context.Logger.LogLine($"Processing request for {input.Function}"); - /// return new BedrockFunctionResponse { Text = "Weather details response" }; - /// }, - /// "Gets detailed weather information" - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, Func handler, string description = "") { - if (handler == null) - throw new ArgumentNullException(nameof(handler)); + ArgumentNullException.ThrowIfNull(handler); if (!CanRegisterTool(name)) return this; @@ -123,26 +92,12 @@ public BedrockAgentFunctionResolver Tool( /// The handler function that accepts input and returns output /// Optional description of the tool function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// resolver.Tool( - /// "GetWeatherDetails", - /// (BedrockFunctionRequest input) => { - /// var city = input.Parameters.FirstOrDefault(p => p.Name == "city")?.Value; - /// return new BedrockFunctionResponse { Text = $"Weather in {city} is sunny" }; - /// }, - /// "Gets weather for a city" - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, Func handler, string description = "") { - if (handler == null) - throw new ArgumentNullException(nameof(handler)); + ArgumentNullException.ThrowIfNull(handler); if (!CanRegisterTool(name)) return this; @@ -158,16 +113,6 @@ public BedrockAgentFunctionResolver Tool( /// The handler function that returns output /// Optional description of the tool function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// resolver.Tool( - /// "GetCurrentTime", - /// () => new BedrockFunctionResponse { Text = DateTime.Now.ToString() }, - /// "Gets the current server time" - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, Func handler, @@ -178,7 +123,7 @@ public BedrockAgentFunctionResolver Tool( if (!CanRegisterTool(name)) return this; - _handlers[name] = (input, context) => handler(); + _handlers[name] = (_, _) => handler(); return this; } @@ -189,16 +134,6 @@ public BedrockAgentFunctionResolver Tool( /// The handler function that returns a string /// Optional description of the tool function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// resolver.Tool( - /// "GetGreeting", - /// () => "Hello, world!", - /// "Returns a greeting message" - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, Func handler, @@ -209,7 +144,7 @@ public BedrockAgentFunctionResolver Tool( if (!CanRegisterTool(name)) return this; - _handlers[name] = (input, context) => BedrockFunctionResponse.WithText( + _handlers[name] = (input, _) => BedrockFunctionResponse.WithText( handler(), input.ActionGroup, name, @@ -226,16 +161,6 @@ public BedrockAgentFunctionResolver Tool( /// The handler function that returns an object /// Optional description of the tool function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// resolver.Tool( - /// "GetServerStatus", - /// () => new { Status = "Online", Uptime = "99.9%" }, - /// "Returns the server status information" - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, Func handler, @@ -246,10 +171,10 @@ public BedrockAgentFunctionResolver Tool( if (!CanRegisterTool(name)) return this; - _handlers[name] = (input, context) => + _handlers[name] = (input, _) => { var result = handler(); - return ConvertToOutput(result, input); + return _resultConverter.ConvertToOutput(result, input); }; return this; } @@ -260,15 +185,6 @@ public BedrockAgentFunctionResolver Tool( /// The name of the tool function /// The delegate handler function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// resolver.Tool( - /// "CalculateSum", - /// (int a, int b) => a + b - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, Delegate handler) @@ -283,16 +199,6 @@ public BedrockAgentFunctionResolver Tool( /// Description of the tool function /// The delegate handler function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// resolver.Tool( - /// "GetWeather", - /// "Gets the weather forecast for a specific city", - /// (string city, int days) => $"{days}-day forecast for {city}: Sunny" - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, string description, @@ -308,15 +214,6 @@ public BedrockAgentFunctionResolver Tool( /// The name of the tool function /// The delegate handler function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// resolver.Tool<int>( - /// "CalculateArea", - /// (int width, int height) => width * height - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, Delegate handler) @@ -332,186 +229,33 @@ public BedrockAgentFunctionResolver Tool( /// Description of the tool function /// The delegate handler function /// The resolver instance for method chaining - /// - /// - /// var resolver = new BedrockAgentFunctionResolver(); - /// - /// // Register a function with strongly typed parameters and return value - /// resolver.Tool<double>( - /// "CalculateDistance", - /// "Calculates the distance between two points", - /// (double x1, double y1, double x2, double y2) => { - /// return Math.Sqrt(Math.Pow(x2 - x1, 2) + Math.Pow(y2 - y1, 2)); - /// } - /// ); - /// - /// // Register a function that accepts Lambda context - /// resolver.Tool<string>( - /// "LogAndReturn", - /// "Logs a message and returns it", - /// (string message, ILambdaContext context) => { - /// context.Logger.LogLine($"Message received: {message}"); - /// return message; - /// } - /// ); - /// - /// public BedrockAgentFunctionResolver Tool( string name, string description, Delegate handler) { - if (handler == null) - throw new ArgumentNullException(nameof(handler)); + ArgumentNullException.ThrowIfNull(handler); if (!CanRegisterTool(name)) return this; - _handlers[name] = (input, context) => - { - var accessor = new ParameterAccessor(input.Parameters); - var parameters = handler.Method.GetParameters(); - var args = new object?[parameters.Length]; - var bedrockParamIndex = 0; - - // Get service provider from resolver if available - var serviceProvider = (this as DiBedrockAgentFunctionResolver)?.ServiceProvider; - - // Map parameters from Bedrock input and DI - for (var i = 0; i < parameters.Length; i++) - { - var parameter = parameters[i]; - var paramType = parameter.ParameterType; - - if (paramType == typeof(ILambdaContext)) - { - args[i] = context; - } - else if (paramType == typeof(BedrockFunctionRequest)) - { - args[i] = input; - } - else if (IsBedrockParameter(paramType)) - { - var paramName = parameter.Name ?? $"arg{bedrockParamIndex}"; - - // AOT-compatible parameter access - direct type checks - // Array parameter handling - if (paramType.IsArray) - { - var jsonArrayStr = accessor.Get(paramName); - - if (!string.IsNullOrEmpty(jsonArrayStr)) - { - try - { - // AOT-compatible deserialization using source generation - if (paramType == typeof(string[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, - BedrockFunctionResolverContext.Default.StringArray); - else if (paramType == typeof(int[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, - BedrockFunctionResolverContext.Default.Int32Array); - else if (paramType == typeof(long[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, - BedrockFunctionResolverContext.Default.Int64Array); - else if (paramType == typeof(double[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, - BedrockFunctionResolverContext.Default.DoubleArray); - else if (paramType == typeof(bool[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, - BedrockFunctionResolverContext.Default.BooleanArray); - else if (paramType == typeof(decimal[])) - args[i] = JsonSerializer.Deserialize(jsonArrayStr, - BedrockFunctionResolverContext.Default.DecimalArray); - else - args[i] = null; // Unsupported array type - } - catch (JsonException) - { - args[i] = null; - } - } - else - { - args[i] = null; - } - } - - if (paramType == typeof(string)) - args[i] = accessor.Get(paramName); - else if (paramType == typeof(int)) - args[i] = accessor.Get(paramName); - else if (paramType == typeof(long)) - args[i] = accessor.Get(paramName); - else if (paramType == typeof(double)) - args[i] = accessor.Get(paramName); - else if (paramType == typeof(bool)) - args[i] = accessor.Get(paramName); - else if (paramType == typeof(decimal)) - args[i] = accessor.Get(paramName); - else if (paramType == typeof(DateTime)) - args[i] = accessor.Get(paramName); - else if (paramType == typeof(Guid)) - args[i] = accessor.Get(paramName); - else if (paramType.IsEnum) - { - // For enums, get as string and parse - var strValue = accessor.Get(paramName); - args[i] = !string.IsNullOrEmpty(strValue) ? Enum.Parse(paramType, strValue) : null; - } - - bedrockParamIndex++; - } - else if (serviceProvider != null) - { - // Resolve from DI - args[i] = serviceProvider.GetService(paramType); - } - } + _handlers[name] = RegisterToolHandler(handler, name); + return this; + } + private Func RegisterToolHandler( + Delegate handler, string functionName) + { + return (input, context) => + { try { - // Execute the handler - var result = handler.DynamicInvoke(args); - - // Direct return for BedrockFunctionResponse - if (result is BedrockFunctionResponse output) - return output; - - // Handle async results with specific type checks (AOT-compatible) - if (result is Task outputTask) - return outputTask.Result; - if (result is Task stringTask) - return ConvertToOutput((TResult)(object)stringTask.Result, input); - if (result is Task intTask) - return ConvertToOutput((TResult)(object)intTask.Result, input); - if (result is Task boolTask) - return ConvertToOutput((TResult)(object)boolTask.Result, input); - if (result is Task doubleTask) - return ConvertToOutput((TResult)(object)doubleTask.Result, input); - if (result is Task longTask) - return ConvertToOutput((TResult)(object)longTask.Result, input); - if (result is Task decimalTask) - return ConvertToOutput((TResult)(object)decimalTask.Result, input); - if (result is Task dateTimeTask) - return ConvertToOutput((TResult)(object)dateTimeTask.Result, input); - if (result is Task guidTask) - return ConvertToOutput((TResult)(object)guidTask.Result, input); - if (result is Task objectTask) - return ConvertToOutput((TResult)objectTask.Result, input); - - // For regular Task with no result - if (result is not Task task) return ConvertToOutput(result, input); - task.GetAwaiter().GetResult(); - return BedrockFunctionResponse.WithText( - string.Empty, - input.ActionGroup, - name, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - + // Map parameters from Bedrock input and DI + var serviceProvider = (this as DiBedrockAgentFunctionResolver)?.ServiceProvider; + var args = _parameterMapper.MapParameters(handler.Method, input, context, serviceProvider); + + // Execute the handler and process result + return ExecuteHandlerAndProcessResult(handler, args, input, context, functionName); } catch (Exception ex) { @@ -519,15 +263,42 @@ public BedrockAgentFunctionResolver Tool( var innerException = ex.InnerException ?? ex; return BedrockFunctionResponse.WithText( $"Error when invoking tool: {innerException.Message}", - input.ActionGroup, - name, + input.ActionGroup, + functionName, input.SessionAttributes, input.PromptSessionAttributes, new Dictionary()); } }; + } - return this; + private BedrockFunctionResponse ExecuteHandlerAndProcessResult( + Delegate handler, + object?[] args, + BedrockFunctionRequest input, + ILambdaContext? context, + string functionName) + { + try + { + // Execute the handler + var result = handler.DynamicInvoke(args); + + // Process various result types + return _resultConverter.ProcessResult(result, input, functionName, context); + } + catch (Exception ex) + { + context?.Logger.LogError(ex.ToString()); + var innerException = ex.InnerException ?? ex; + return BedrockFunctionResponse.WithText( + $"Error when invoking tool: {innerException.Message}", + input.ActionGroup, + functionName, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } } /// @@ -536,19 +307,6 @@ public BedrockAgentFunctionResolver Tool( /// The Bedrock Agent input containing the function name and parameters /// Optional Lambda context /// The output from the function execution - /// - /// - /// // Lambda handler - /// public BedrockFunctionResponse FunctionHandler(BedrockFunctionRequest input, ILambdaContext context) - /// { - /// var resolver = new BedrockAgentFunctionResolver() - /// .Tool("GetWeather", (string city) => $"Weather in {city} is sunny") - /// .Tool("GetTime", () => DateTime.Now.ToString()); - /// - /// return resolver.Resolve(input, context); - /// } - /// - /// public BedrockFunctionResponse Resolve(BedrockFunctionRequest input, ILambdaContext? context = null) { return ResolveAsync(input, context).GetAwaiter().GetResult(); @@ -560,23 +318,6 @@ public BedrockFunctionResponse Resolve(BedrockFunctionRequest input, ILambdaCont /// The Bedrock Agent input containing the function name and parameters /// Optional Lambda context /// A task that completes with the output from the function execution - /// - /// - /// // Async Lambda handler - /// public async Task<BedrockFunctionResponse> FunctionHandler(BedrockFunctionRequest input, ILambdaContext context) - /// { - /// var resolver = new BedrockAgentFunctionResolver() - /// .Tool("GetWeatherAsync", async (string city) => { - /// // Simulate API call - /// await Task.Delay(100); - /// return $"Weather in {city} is sunny"; - /// }) - /// .Tool("GetTime", () => DateTime.Now.ToString()); - /// - /// return await resolver.ResolveAsync(input, context); - /// } - /// - /// public async Task ResolveAsync(BedrockFunctionRequest input, ILambdaContext? context = null) { @@ -624,115 +365,5 @@ private BedrockFunctionResponse HandleEvent(BedrockFunctionRequest input, ILambd input.PromptSessionAttributes, new Dictionary()); } - - private BedrockFunctionResponse ConvertToOutput(T result, BedrockFunctionRequest input) - { - string actionGroup = input.ActionGroup; - string function = input.Function; - - if (result == null) - { - return BedrockFunctionResponse.WithText( - string.Empty, - actionGroup, - function, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } - - // If result is already an BedrockFunctionResponse, ensure action group and function are set - if (result is BedrockFunctionResponse output) - { - // If the action group or function are not set in the output, use the provided values - if (string.IsNullOrEmpty(output.Response.ActionGroup)) - { - output.Response.ActionGroup = actionGroup; - } - - if (string.IsNullOrEmpty(output.Response.Function)) - { - output.Response.Function = function; - } - - return output; - } - - // For primitive types and strings, convert to string - if (result is string str) - { - return BedrockFunctionResponse.WithText( - str, - actionGroup, - function, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } - - if (result is int intVal) - { - return BedrockFunctionResponse.WithText( - intVal.ToString(CultureInfo.InvariantCulture), - actionGroup, - function, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } - - if (result is double doubleVal) - { - return BedrockFunctionResponse.WithText( - doubleVal.ToString(CultureInfo.InvariantCulture), - actionGroup, - function, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } - - if (result is bool boolVal) - { - return BedrockFunctionResponse.WithText( - boolVal.ToString(), - actionGroup, - function, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } - - if (result is long longVal) - { - return BedrockFunctionResponse.WithText( - longVal.ToString(CultureInfo.InvariantCulture), - actionGroup, - function, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } - - if (result is decimal decimalVal) - { - return BedrockFunctionResponse.WithText( - decimalVal.ToString(CultureInfo.InvariantCulture), - actionGroup, - function, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } - - // For any other type, use ToString() - return BedrockFunctionResponse.WithText( - result.ToString() ?? string.Empty, - actionGroup, - function, - input.SessionAttributes, - input.PromptSessionAttributes, - new Dictionary()); - } } } diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs new file mode 100644 index 00000000..85cb1a3e --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs @@ -0,0 +1,150 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Reflection; +using System.Text.Json; +using Amazon.Lambda.Core; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers +{ + /// + /// Maps parameters for Bedrock Agent function handlers + /// + public class ParameterMapper + { + private readonly ParameterTypeValidator _validator = new(); + + /// + /// Maps parameters for a handler method from a Bedrock function request + /// + /// The handler method + /// The Bedrock function request + /// The Lambda context + /// Optional service provider for dependency injection + /// Array of arguments to pass to the handler + public object?[] MapParameters( + MethodInfo methodInfo, + BedrockFunctionRequest input, + ILambdaContext? context, + IServiceProvider? serviceProvider) + { + var parameters = methodInfo.GetParameters(); + var args = new object?[parameters.Length]; + var accessor = new ParameterAccessor(input.Parameters); + var bedrockParamIndex = 0; + + for (var i = 0; i < parameters.Length; i++) + { + var parameter = parameters[i]; + var paramType = parameter.ParameterType; + + if (paramType == typeof(ILambdaContext)) + { + args[i] = context; + } + else if (paramType == typeof(BedrockFunctionRequest)) + { + args[i] = input; + } + else if (_validator.IsBedrockParameter(paramType)) + { + args[i] = MapBedrockParameter(paramType, parameter.Name ?? $"arg{bedrockParamIndex}", accessor); + bedrockParamIndex++; + } + else if (serviceProvider != null) + { + // Resolve from DI + args[i] = serviceProvider.GetService(paramType); + } + } + + return args; + } + + private object? MapBedrockParameter(Type paramType, string paramName, ParameterAccessor accessor) + { + // Array parameter handling + if (paramType.IsArray) + { + return MapArrayParameter(paramType, paramName, accessor); + } + + // Scalar parameter handling + return MapScalarParameter(paramType, paramName, accessor); + } + + private object? MapArrayParameter(Type paramType, string paramName, ParameterAccessor accessor) + { + var jsonArrayStr = accessor.Get(paramName); + + if (string.IsNullOrEmpty(jsonArrayStr)) + { + return null; + } + + try + { + // AOT-compatible deserialization using source generation + if (paramType == typeof(string[])) + return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.StringArray); + if (paramType == typeof(int[])) + return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.Int32Array); + if (paramType == typeof(long[])) + return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.Int64Array); + if (paramType == typeof(double[])) + return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.DoubleArray); + if (paramType == typeof(bool[])) + return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.BooleanArray); + if (paramType == typeof(decimal[])) + return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.DecimalArray); + } + catch (JsonException) + { + // Return null on error + } + + return null; + } + + private object? MapScalarParameter(Type paramType, string paramName, ParameterAccessor accessor) + { + if (paramType == typeof(string)) + return accessor.Get(paramName); + if (paramType == typeof(int)) + return accessor.Get(paramName); + if (paramType == typeof(long)) + return accessor.Get(paramName); + if (paramType == typeof(double)) + return accessor.Get(paramName); + if (paramType == typeof(bool)) + return accessor.Get(paramName); + if (paramType == typeof(decimal)) + return accessor.Get(paramName); + if (paramType == typeof(DateTime)) + return accessor.Get(paramName); + if (paramType == typeof(Guid)) + return accessor.Get(paramName); + if (paramType.IsEnum) + { + // For enums, get as string and parse + var strValue = accessor.Get(paramName); + return !string.IsNullOrEmpty(strValue) ? Enum.Parse(paramType, strValue) : null; + } + + return null; + } + } +} diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterTypeValidator.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterTypeValidator.cs new file mode 100644 index 00000000..19123d49 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterTypeValidator.cs @@ -0,0 +1,50 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers +{ + /// + /// Validates parameter types for Bedrock Agent functions + /// + public class ParameterTypeValidator + { + private static readonly HashSet BedrockParameterTypes = new() + { + typeof(string), + typeof(int), + typeof(long), + typeof(double), + typeof(bool), + typeof(decimal), + typeof(DateTime), + typeof(Guid), + typeof(string[]), + typeof(int[]), + typeof(long[]), + typeof(double[]), + typeof(bool[]), + typeof(decimal[]) + }; + + /// + /// Checks if a type is a valid Bedrock parameter type + /// + /// The type to check + /// True if the type is valid for Bedrock parameters + public bool IsBedrockParameter(Type type) => + BedrockParameterTypes.Contains(type) || type.IsEnum || + (type.IsArray && BedrockParameterTypes.Contains(type.GetElementType()!)); + } +} diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs new file mode 100644 index 00000000..65f5d9ed --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs @@ -0,0 +1,238 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Globalization; +using Amazon.Lambda.Core; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers +{ + /// + /// Converts handler results to BedrockFunctionResponse + /// + public class ResultConverter + { + /// + /// Processes results from handler functions and converts to BedrockFunctionResponse + /// + public BedrockFunctionResponse ProcessResult( + object? result, + BedrockFunctionRequest input, + string functionName, + ILambdaContext? context) + { + // Direct return for BedrockFunctionResponse + if (result is BedrockFunctionResponse output) + return EnsureResponseMetadata(output, input, functionName); + + // Handle async results with specific type checks (AOT-compatible) + if (result is Task outputTask) + return EnsureResponseMetadata(outputTask.Result, input, functionName); + + // Handle various Task types + if (result is Task task) + { + return HandleTaskResult(task, input, functionName); + } + + // Handle regular (non-task) results + return ConvertToOutput(result, input); + } + + private BedrockFunctionResponse HandleTaskResult(Task task, BedrockFunctionRequest input, string functionName) + { + // For Task + if (task is Task stringTask) + return ConvertToOutput((TResult)(object)stringTask.Result, input); + + // For Task + if (task is Task intTask) + return ConvertToOutput((TResult)(object)intTask.Result, input); + + // For Task + if (task is Task boolTask) + return ConvertToOutput((TResult)(object)boolTask.Result, input); + + // For Task + if (task is Task doubleTask) + return ConvertToOutput((TResult)(object)doubleTask.Result, input); + + // For Task + if (task is Task longTask) + return ConvertToOutput((TResult)(object)longTask.Result, input); + + // For Task + if (task is Task decimalTask) + return ConvertToOutput((TResult)(object)decimalTask.Result, input); + + // For Task + if (task is Task dateTimeTask) + return ConvertToOutput((TResult)(object)dateTimeTask.Result, input); + + // For Task + if (task is Task guidTask) + return ConvertToOutput((TResult)(object)guidTask.Result, input); + + // For Task + if (task is Task objectTask) + return ConvertToOutput((TResult)objectTask.Result, input); + + // For regular Task with no result + task.GetAwaiter().GetResult(); + return BedrockFunctionResponse.WithText( + string.Empty, + input.ActionGroup, + input.Function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + /// + /// Converts a result to a BedrockFunctionResponse + /// + public BedrockFunctionResponse ConvertToOutput(T result, BedrockFunctionRequest input) + { + string actionGroup = input.ActionGroup; + string function = input.Function; + + if (result == null) + { + return CreateEmptyResponse(input); + } + + // If result is already a BedrockFunctionResponse, ensure metadata is set + if (result is BedrockFunctionResponse output) + { + return EnsureResponseMetadata(output, input, function); + } + + // Handle primitive types + return ConvertPrimitiveToOutput(result, input); + } + + private BedrockFunctionResponse ConvertPrimitiveToOutput(T result, BedrockFunctionRequest input) + { + string actionGroup = input.ActionGroup; + string function = input.Function; + + // For primitive types and strings, convert to string + if (result is string str) + { + return BedrockFunctionResponse.WithText( + str, + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + if (result is int intVal) + { + return BedrockFunctionResponse.WithText( + intVal.ToString(CultureInfo.InvariantCulture), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + if (result is double doubleVal) + { + return BedrockFunctionResponse.WithText( + doubleVal.ToString(CultureInfo.InvariantCulture), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + if (result is bool boolVal) + { + return BedrockFunctionResponse.WithText( + boolVal.ToString(), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + if (result is long longVal) + { + return BedrockFunctionResponse.WithText( + longVal.ToString(CultureInfo.InvariantCulture), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + if (result is decimal decimalVal) + { + return BedrockFunctionResponse.WithText( + decimalVal.ToString(CultureInfo.InvariantCulture), + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + // For any other type, use ToString() + return BedrockFunctionResponse.WithText( + result?.ToString() ?? string.Empty, + actionGroup, + function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + private BedrockFunctionResponse CreateEmptyResponse(BedrockFunctionRequest input) + { + return BedrockFunctionResponse.WithText( + string.Empty, + input.ActionGroup, + input.Function, + input.SessionAttributes, + input.PromptSessionAttributes, + new Dictionary()); + } + + private BedrockFunctionResponse EnsureResponseMetadata( + BedrockFunctionResponse response, + BedrockFunctionRequest input, + string functionName) + { + // If the action group or function are not set in the output, use the provided values + if (string.IsNullOrEmpty(response.Response.ActionGroup)) + { + response.Response.ActionGroup = input.ActionGroup; + } + + if (string.IsNullOrEmpty(response.Response.Function)) + { + response.Response.Function = functionName; + } + + return response; + } + } +} From 92dbf72b80c12d0d97de73d6317f013b40cff38e Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 May 2025 09:39:17 +0100 Subject: [PATCH 39/52] refactor sonar warnings --- .../Helpers/ResultConverter.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs index 65f5d9ed..ee668b28 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs @@ -44,14 +44,14 @@ public BedrockFunctionResponse ProcessResult( // Handle various Task types if (result is Task task) { - return HandleTaskResult(task, input, functionName); + return HandleTaskResult(task, input); } // Handle regular (non-task) results return ConvertToOutput(result, input); } - private BedrockFunctionResponse HandleTaskResult(Task task, BedrockFunctionRequest input, string functionName) + private BedrockFunctionResponse HandleTaskResult(Task task, BedrockFunctionRequest input) { // For Task if (task is Task stringTask) @@ -105,10 +105,9 @@ private BedrockFunctionResponse HandleTaskResult(Task task, BedrockFunc /// public BedrockFunctionResponse ConvertToOutput(T result, BedrockFunctionRequest input) { - string actionGroup = input.ActionGroup; - string function = input.Function; + var function = input.Function; - if (result == null) + if (EqualityComparer.Default.Equals(result, default(T))) { return CreateEmptyResponse(input); } @@ -125,8 +124,8 @@ public BedrockFunctionResponse ConvertToOutput(T result, BedrockFunctionReque private BedrockFunctionResponse ConvertPrimitiveToOutput(T result, BedrockFunctionRequest input) { - string actionGroup = input.ActionGroup; - string function = input.Function; + var actionGroup = input.ActionGroup; + var function = input.Function; // For primitive types and strings, convert to string if (result is string str) From eb211792e4fac897a7cc044cd12665ea2514b8e8 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 May 2025 10:08:37 +0100 Subject: [PATCH 40/52] add unit tests for ParameterAccessor, ParameterMapper, ParameterTypeValidator, ResultConverter, and BedrockAgentFunctionResolver --- ...ambda.Powertools.EventHandler.Tests.csproj | 6 +- ...ockAgentFunctionResolverAdditionalTests.cs | 212 ++++++++++++ .../BedrockAgentFunctionResolverTests.cs | 5 +- .../Helpers/ParameterAccessorTests.cs | 197 +++++++++++ .../Helpers/ParameterMapperTests.cs | 311 ++++++++++++++++++ .../Helpers/ParameterTypeValidatorTests.cs | 49 +++ .../Helpers/ResultConverterTests.cs | 276 ++++++++++++++++ .../bedrockFunctionEvent.json | 0 .../{ => EventHandler}/AppSyncEventsTests.cs | 2 +- .../RouteHandlerRegistryTests.cs | 2 +- .../bedrockFunctionEvent2.json | 27 -- 11 files changed, 1052 insertions(+), 35 deletions(-) create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverAdditionalTests.cs rename libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/{ => BedrockAgentFunction}/BedrockAgentFunctionResolverTests.cs (99%) create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterAccessorTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterMapperTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterTypeValidatorTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ResultConverterTests.cs rename libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/{ => BedrockAgentFunction}/bedrockFunctionEvent.json (100%) rename libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/{ => EventHandler}/AppSyncEventsTests.cs (99%) rename libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/{ => EventHandler}/RouteHandlerRegistryTests.cs (99%) delete mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj index 1d0b8362..2d37bab6 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AWS.Lambda.Powertools.EventHandler.Tests.csproj @@ -17,6 +17,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -42,11 +43,8 @@ PreserveNewest - - PreserveNewest - - + PreserveNewest diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverAdditionalTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverAdditionalTests.cs new file mode 100644 index 00000000..62d09fde --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverAdditionalTests.cs @@ -0,0 +1,212 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.EventHandler.Resolvers; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +namespace AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction +{ + public class BedrockAgentFunctionResolverAdditionalTests + { + [Fact] + public async Task ResolveAsync_WithValidInput_ReturnsResult() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("AsyncTest", () => "Async result"); + + var input = new BedrockFunctionRequest { Function = "AsyncTest" }; + var context = new TestLambdaContext(); + + // Act + var result = await resolver.ResolveAsync(input, context); + + // Assert + Assert.Equal("Async result", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void Tool_WithNullHandler_ThrowsException() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + Func nullHandler = null!; + + // Act/Assert + Assert.Throws(() => resolver.Tool("NullTest", nullHandler)); + } + + [Fact] + public void Resolve_WithNullFunction_ReturnsErrorResponse() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + var input = new BedrockFunctionRequest { Function = null }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("No tool specified in the request", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void Resolve_WithEmptyFunction_ReturnsErrorResponse() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + var input = new BedrockFunctionRequest { Function = "" }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("No tool specified in the request", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void Tool_WithHandlerThrowingException_ReturnsErrorResponse() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("ExceptionTest", (BedrockFunctionRequest input, ILambdaContext ctx) => { + throw new InvalidOperationException("Handler exception"); + return new BedrockFunctionResponse(); + }); + + var input = new BedrockFunctionRequest { Function = "ExceptionTest" }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Error when invoking tool: Handler exception", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void Tool_WithDynamicInvokeException_ReturnsErrorResponse() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("ExceptionTest", (Func)(() => { + throw new InvalidOperationException("Dynamic invoke exception"); + })); + + var input = new BedrockFunctionRequest { Function = "ExceptionTest" }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains("Error when invoking tool", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void Tool_ObjectFunctionRegistration_ReturnsObjectAsString() + { + // Arrange + var testObject = new TestObject { Id = 123, Name = "Test" }; + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("ObjectTest", () => testObject); + + var input = new BedrockFunctionRequest { Function = "ObjectTest" }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal(testObject.ToString(), result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public async Task Resolve_WithAsyncTask_HandlesCorrectly() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("AsyncTaskTest", async (string message) => { + await Task.Delay(10); // Simulate async work + return $"Processed: {message}"; + }); + + var input = new BedrockFunctionRequest { + Function = "AsyncTaskTest", + Parameters = new List { + new Parameter { Name = "message", Value = "hello", Type = "String" } + } + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Processed: hello", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void Tool_WithBedrockFunctionResponseHandlerNoContext_MapsCorrectly() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("NoContextTest", (BedrockFunctionRequest request) => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "NoContextTest", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "No context needed" } + } + } + } + }); + + var input = new BedrockFunctionRequest { Function = "NoContextTest" }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("No context needed", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void Tool_WithBedrockFunctionResponseHandler_MapsCorrectly() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + resolver.Tool("ResponseTest", () => new BedrockFunctionResponse + { + Response = new Response + { + ActionGroup = "TestGroup", + Function = "ResponseTest", + FunctionResponse = new FunctionResponse + { + ResponseBody = new ResponseBody + { + Text = new TextBody { Body = "Direct response" } + } + } + } + }); + + var input = new BedrockFunctionRequest { Function = "ResponseTest" }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Equal("Direct response", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + private class TestObject + { + public int Id { get; set; } + public string Name { get; set; } = ""; + + public override string ToString() => $"{Name} (ID: {Id})"; + } + } +} diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverTests.cs similarity index 99% rename from libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs rename to libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverTests.cs index 50b28fe6..5e630214 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverTests.cs @@ -2,13 +2,14 @@ using System.Text; using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.EventHandler.Resolvers; using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; using Microsoft.Extensions.DependencyInjection; #pragma warning disable CS0162 // Unreachable code detected -// ReSharper disable once CheckNamespace -namespace AWS.Lambda.Powertools.EventHandler.Resolvers.Tests; + +namespace AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction; public class BedrockAgentFunctionResolverTests { diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterAccessorTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterAccessorTests.cs new file mode 100644 index 00000000..34a8f246 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterAccessorTests.cs @@ -0,0 +1,197 @@ +using AWS.Lambda.Powertools.EventHandler.Resolvers; + +namespace AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction.Helpers +{ + public class ParameterAccessorTests + { + [Fact] + public void Get_WithStringParameter_ReturnsValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "name", Value = "TestValue", Type = "String" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.Get("name"); + + // Assert + Assert.Equal("TestValue", result); + } + + [Fact] + public void Get_WithIntParameter_ReturnsValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "age", Value = "30", Type = "Number" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.Get("age"); + + // Assert + Assert.Equal(30, result); + } + + [Fact] + public void Get_WithBoolParameter_ReturnsValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "active", Value = "true", Type = "Boolean" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.Get("active"); + + // Assert + Assert.True(result); + } + + [Fact] + public void Get_WithLongParameter_ReturnsValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "bigNumber", Value = "9223372036854775807", Type = "Number" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.Get("bigNumber"); + + // Assert + Assert.Equal(9223372036854775807, result); + } + + [Fact] + public void Get_WithDoubleParameter_ReturnsValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "price", Value = "99.99", Type = "Number" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.Get("price"); + + // Assert + Assert.Equal(99.99, result); + } + + [Fact] + public void Get_WithDecimalParameter_ReturnsValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "amount", Value = "123.456", Type = "Number" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.Get("amount"); + + // Assert + Assert.Equal(123.456m, result); + } + + [Fact] + public void Get_WithNonExistentParameter_ReturnsDefault() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "existing", Value = "value", Type = "String" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var stringResult = accessor.Get("nonExistent"); + var intResult = accessor.Get("nonExistent"); + var boolResult = accessor.Get("nonExistent"); + + // Assert + Assert.Null(stringResult); + Assert.Equal(0, intResult); + Assert.False(boolResult); + } + + [Fact] + public void Get_WithCaseSensitivity_WorksCaseInsensitively() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "userName", Value = "John", Type = "String" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result1 = accessor.Get("userName"); + var result2 = accessor.Get("UserName"); + var result3 = accessor.Get("USERNAME"); + + // Assert + Assert.Equal("John", result1); + Assert.Equal("John", result2); + Assert.Equal("John", result3); + } + + [Fact] + public void Get_WithNullParameters_ReturnsDefault() + { + // Arrange + var accessor = new ParameterAccessor(null); + + // Act + var stringResult = accessor.Get("any"); + var intResult = accessor.Get("any"); + + // Assert + Assert.Null(stringResult); + Assert.Equal(0, intResult); + } + + [Fact] + public void Get_WithInvalidType_ReturnsDefault() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "number", Value = "not-a-number", Type = "Number" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.Get("number"); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void Get_WithEmptyParameters_ReturnsDefault() + { + // Arrange + var parameters = new List(); + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.Get("anything"); + + // Assert + Assert.Null(result); + } + } +} diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterMapperTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterMapperTests.cs new file mode 100644 index 00000000..b4cd5705 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterMapperTests.cs @@ -0,0 +1,311 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.EventHandler.Resolvers; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; +using NSubstitute; + +namespace AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction.Helpers +{ + public class ParameterMapperTests + { + private readonly ParameterMapper _mapper = new(); + + [Fact] + public void MapParameters_WithNoParameters_ReturnsEmptyArray() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.NoParameters))!; + var input = new BedrockFunctionRequest(); + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Empty(result); + } + + [Fact] + public void MapParameters_WithLambdaContext_MapsCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithLambdaContext))!; + var input = new BedrockFunctionRequest(); + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + Assert.Same(context, result[0]); + } + + [Fact] + public void MapParameters_WithBedrockFunctionRequest_MapsCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithBedrockFunctionRequest))!; + var input = new BedrockFunctionRequest(); + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + Assert.Same(input, result[0]); + } + + [Fact] + public void MapParameters_WithStringParameter_MapsCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithStringParameter))!; + var input = new BedrockFunctionRequest + { + Parameters = new List + { + new() { Name = "name", Value = "TestValue", Type = "String" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + Assert.Equal("TestValue", result[0]); + } + + [Fact] + public void MapParameters_WithIntParameter_MapsCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithIntParameter))!; + var input = new BedrockFunctionRequest + { + Parameters = new List + { + new() { Name = "value", Value = "42", Type = "Number" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + Assert.Equal(42, result[0]); + } + + [Fact] + public void MapParameters_WithBoolParameter_MapsCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithBoolParameter))!; + var input = new BedrockFunctionRequest + { + Parameters = new List + { + new() { Name = "flag", Value = "true", Type = "Boolean" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + Assert.True((bool)result[0]!); + } + + [Fact] + public void MapParameters_WithEnumParameter_MapsCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithEnumParameter))!; + var input = new BedrockFunctionRequest + { + Parameters = new List + { + new() { Name = "testEnum", Value = "Option2", Type = "String" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + Assert.Equal(TestEnum.Option2, result[0]); + } + + [Fact] + public void MapParameters_WithStringArrayParameter_MapsCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithStringArrayParameter))!; + var input = new BedrockFunctionRequest + { + Parameters = new List + { + new() { Name = "values", Value = "[\"one\",\"two\",\"three\"]", Type = "String" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + var array = (string[])result[0]!; + Assert.Equal(3, array.Length); + Assert.Equal("one", array[0]); + Assert.Equal("two", array[1]); + Assert.Equal("three", array[2]); + } + + [Fact] + public void MapParameters_WithIntArrayParameter_MapsCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithIntArrayParameter))!; + var input = new BedrockFunctionRequest + { + Parameters = new List + { + new() { Name = "values", Value = "[1,2,3]", Type = "String" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + var array = (int[])result[0]!; + Assert.Equal(3, array.Length); + Assert.Equal(1, array[0]); + Assert.Equal(2, array[1]); + Assert.Equal(3, array[2]); + } + + [Fact] + public void MapParameters_WithInvalidJsonArray_ReturnsNull() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithStringArrayParameter))!; + var input = new BedrockFunctionRequest + { + Parameters = new List + { + new() { Name = "values", Value = "[invalid json]", Type = "String" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Single(result); + Assert.Null(result[0]); + } + + [Fact] + public void MapParameters_WithServiceProvider_ResolvesService() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithDependencyInjection))!; + var input = new BedrockFunctionRequest(); + var context = new TestLambdaContext(); + + // Create a test service + var testService = new TestService(); + + // Setup service provider + var serviceProvider = Substitute.For(); + serviceProvider.GetService(typeof(ITestService)).Returns(testService); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, serviceProvider); + + // Assert + Assert.Equal(3, result.Length); + Assert.Same(context, result[0]); + Assert.Same(input, result[1]); + Assert.Same(testService, result[2]); + } + + [Fact] + public void MapParameters_WithMultipleParameterTypes_MapsAllCorrectly() + { + // Arrange + var methodInfo = typeof(TestMethodsClass).GetMethod(nameof(TestMethodsClass.WithMultipleParameterTypes))!; + var input = new BedrockFunctionRequest + { + Parameters = new List + { + new() { Name = "name", Value = "TestUser", Type = "String" }, + new() { Name = "age", Value = "30", Type = "Number" }, + new() { Name = "isActive", Value = "true", Type = "Boolean" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = _mapper.MapParameters(methodInfo, input, context, null); + + // Assert + Assert.Equal(4, result.Length); + Assert.Equal("TestUser", result[0]); + Assert.Equal(30, result[1]); + Assert.True((bool)result[2]!); + Assert.Same(context, result[3]); + } + + public class TestMethodsClass + { + public void NoParameters() { } + + public void WithLambdaContext(ILambdaContext context) { } + + public void WithBedrockFunctionRequest(BedrockFunctionRequest request) { } + + public void WithStringParameter(string name) { } + + public void WithIntParameter(int value) { } + + public void WithBoolParameter(bool flag) { } + + public void WithEnumParameter(TestEnum testEnum) { } + + public void WithStringArrayParameter(string[] values) { } + + public void WithIntArrayParameter(int[] values) { } + + public void WithDependencyInjection(ILambdaContext context, BedrockFunctionRequest request, ITestService service) { } + + public void WithMultipleParameterTypes(string name, int age, bool isActive, ILambdaContext context) { } + } + + public interface ITestService { } + + public class TestService : ITestService { } + + public enum TestEnum + { + Option1, + Option2, + Option3 + } + } +} diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterTypeValidatorTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterTypeValidatorTests.cs new file mode 100644 index 00000000..b8ec3353 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterTypeValidatorTests.cs @@ -0,0 +1,49 @@ +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers; + +namespace AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction.Helpers +{ + public class ParameterTypeValidatorTests + { + private readonly ParameterTypeValidator _validator = new(); + + [Theory] + [InlineData(typeof(string), true)] + [InlineData(typeof(int), true)] + [InlineData(typeof(long), true)] + [InlineData(typeof(double), true)] + [InlineData(typeof(bool), true)] + [InlineData(typeof(decimal), true)] + [InlineData(typeof(DateTime), true)] + [InlineData(typeof(Guid), true)] + [InlineData(typeof(string[]), true)] + [InlineData(typeof(int[]), true)] + [InlineData(typeof(long[]), true)] + [InlineData(typeof(double[]), true)] + [InlineData(typeof(bool[]), true)] + [InlineData(typeof(decimal[]), true)] + [InlineData(typeof(TestEnum), true)] // Enum should be valid + [InlineData(typeof(object), false)] + [InlineData(typeof(Dictionary), false)] + [InlineData(typeof(List), false)] + [InlineData(typeof(float), false)] + [InlineData(typeof(char), false)] + [InlineData(typeof(byte), false)] + [InlineData(typeof(float[]), false)] + [InlineData(typeof(object[]), false)] + public void IsBedrockParameter_WithVariousTypes_ReturnsExpectedResult(Type type, bool expected) + { + // Act + var result = _validator.IsBedrockParameter(type); + + // Assert + Assert.Equal(expected, result); + } + + private enum TestEnum + { + One, + Two, + Three + } + } +} diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ResultConverterTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ResultConverterTests.cs new file mode 100644 index 00000000..437118e5 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ResultConverterTests.cs @@ -0,0 +1,276 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +namespace AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction.Helpers +{ + public class ResultConverterTests + { + private readonly ResultConverter _converter = new(); + private readonly BedrockFunctionRequest _defaultInput = new() + { + Function = "TestFunction", + ActionGroup = "TestGroup", + SessionAttributes = new Dictionary { { "testKey", "testValue" } }, + PromptSessionAttributes = new Dictionary { { "promptKey", "promptValue" } } + }; + private readonly string _functionName = "TestFunction"; + private readonly ILambdaContext _context = new TestLambdaContext(); + + [Fact] + public void ProcessResult_WithBedrockFunctionResponse_ReturnsUnchanged() + { + // Arrange + var response = BedrockFunctionResponse.WithText( + "Test response", + "TestGroup", + "TestFunction", + new Dictionary(), + new Dictionary(), + new Dictionary()); + + // Act + var result = _converter.ProcessResult(response, _defaultInput, _functionName, _context); + + // Assert + Assert.Same(response, result); + } + + [Fact] + public void ProcessResult_WithNullValue_ReturnsEmptyResponse() + { + // Arrange + object? nullValue = null; + + // Act + var result = _converter.ProcessResult(nullValue, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal(string.Empty, result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal(_defaultInput.ActionGroup, result.Response.ActionGroup); + Assert.Equal(_defaultInput.Function, result.Response.Function); + } + + [Fact] + public void ProcessResult_WithStringValue_ReturnsTextResponse() + { + // Arrange + var stringValue = "Hello, world!"; + + // Act + var result = _converter.ProcessResult(stringValue, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal(stringValue, result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void ProcessResult_WithIntValue_ReturnsTextResponse() + { + // Arrange + var intValue = 42; + + // Act + var result = _converter.ProcessResult(intValue, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("42", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void ProcessResult_WithDecimalValue_ReturnsTextResponse() + { + // Arrange + var decimalValue = 42.5m; + + // Act + var result = _converter.ProcessResult(decimalValue, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("42.5", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void ProcessResult_WithBoolValue_ReturnsTextResponse() + { + // Arrange + var boolValue = true; + + // Act + var result = _converter.ProcessResult(boolValue, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("True", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void ProcessResult_WithObjectValue_ReturnsToString() + { + // Arrange + var testObject = new TestObject { Name = "Test", Value = 42 }; + + // Act + var result = _converter.ProcessResult(testObject, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal(testObject.ToString(), result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public async Task ProcessResult_WithTaskStringResult_ReturnsTextResponse() + { + // Arrange + Task task = Task.FromResult("Async result"); + + // Act + var result = _converter.ProcessResult(task, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("Async result", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public async Task ProcessResult_WithTaskIntResult_ReturnsTextResponse() + { + // Arrange + Task task = Task.FromResult(42); + + // Act + var result = _converter.ProcessResult(task, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("42", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public async Task ProcessResult_WithTaskBoolResult_ReturnsTextResponse() + { + // Arrange + Task task = Task.FromResult(true); + + // Act + var result = _converter.ProcessResult(task, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("True", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public async Task ProcessResult_WithVoidTask_ReturnsEmptyResponse() + { + // Arrange + Task task = Task.CompletedTask; + + // Act + var result = _converter.ProcessResult(task, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal(string.Empty, result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public async Task ProcessResult_WithTaskBedrockResponse_ReturnsResponse() + { + // Arrange + var response = BedrockFunctionResponse.WithText( + "Async response", + "AsyncGroup", + "AsyncFunction", + new Dictionary(), + new Dictionary(), + new Dictionary()); + + Task task = Task.FromResult(response); + + // Act + var result = _converter.ProcessResult(task, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("Async response", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("AsyncGroup", result.Response.ActionGroup); + Assert.Equal("AsyncFunction", result.Response.Function); + } + + [Fact] + public void EnsureResponseMetadata_WithEmptyMetadata_FillsFromInput() + { + // Arrange + var response = BedrockFunctionResponse.WithText( + "Test response", + "", // Empty action group + "", // Empty function name + _defaultInput.SessionAttributes, + _defaultInput.PromptSessionAttributes, + new Dictionary()); + + // Act + var result = _converter.ConvertToOutput(response, _defaultInput); + + // Assert + Assert.Equal("Test response", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal(_defaultInput.ActionGroup, result.Response.ActionGroup); // Filled from input + Assert.Equal(_defaultInput.Function, result.Response.Function); // Filled from input + } + + [Fact] + public void ConvertToOutput_PreservesSessionAttributes() + { + // Arrange + var sessionAttributes = new Dictionary { { "userID", "test123" } }; + var promptAttributes = new Dictionary { { "context", "testing" } }; + + var input = new BedrockFunctionRequest + { + Function = "TestFunction", + ActionGroup = "TestGroup", + SessionAttributes = sessionAttributes, + PromptSessionAttributes = promptAttributes + }; + + // Act + var result = _converter.ConvertToOutput("Test response", input); + + // Assert + Assert.Equal(sessionAttributes, result.SessionAttributes); + Assert.Equal(promptAttributes, result.PromptSessionAttributes); + } + + [Fact] + public void ProcessResult_WithLongValue_ReturnsTextResponse() + { + // Arrange + long longValue = 9223372036854775807; + + // Act + var result = _converter.ProcessResult(longValue, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("9223372036854775807", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void ProcessResult_WithDoubleValue_ReturnsTextResponse() + { + // Arrange + double doubleValue = 123.456; + + // Act + var result = _converter.ProcessResult(doubleValue, _defaultInput, _functionName, _context); + + // Assert + Assert.Equal("123.456", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + private class TestObject + { + public string Name { get; set; } = ""; + public int Value { get; set; } + + public override string ToString() + { + return $"{Name}:{Value}"; + } + } + } +} diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent.json b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/bedrockFunctionEvent.json similarity index 100% rename from libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent.json rename to libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/bedrockFunctionEvent.json diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AppSyncEventsTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/EventHandler/AppSyncEventsTests.cs similarity index 99% rename from libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AppSyncEventsTests.cs rename to libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/EventHandler/AppSyncEventsTests.cs index 0b8103f4..07c0e9fa 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/AppSyncEventsTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/EventHandler/AppSyncEventsTests.cs @@ -6,7 +6,7 @@ #pragma warning disable CS8604 // Possible null reference argument. #pragma warning disable CS8602 // Dereference of a possibly null reference. -namespace AWS.Lambda.Powertools.EventHandler.Tests; +namespace AWS.Lambda.Powertools.EventHandler; public class AppSyncEventsTests { diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/RouteHandlerRegistryTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/EventHandler/RouteHandlerRegistryTests.cs similarity index 99% rename from libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/RouteHandlerRegistryTests.cs rename to libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/EventHandler/RouteHandlerRegistryTests.cs index f0437dc9..ac712da6 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/RouteHandlerRegistryTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/EventHandler/RouteHandlerRegistryTests.cs @@ -5,7 +5,7 @@ #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. #pragma warning disable CS8602 // Dereference of a possibly null reference. -namespace AWS.Lambda.Powertools.EventHandler.Tests; +namespace AWS.Lambda.Powertools.EventHandler; [SuppressMessage("Usage", "xUnit1031:Do not use blocking task operations in test method")] public class RouteHandlerRegistryTests diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json deleted file mode 100644 index 18e21694..00000000 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/bedrockFunctionEvent2.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "messageVersion": "1.0", - "function": "get_weather_city", - "parameters": [ - { - "name": "month", - "type": "number", - "value": "5" - }, - { - "name": "city", - "type": "string", - "value": "Lisbon" - } - ], - "sessionId": "533568316194812", - "agent": { - "name": "powertools-function-agent", - "version": "DRAFT", - "id": "AVMWXZYN4X", - "alias": "TSTALIASID" - }, - "actionGroup": "action_group_quick_start_hgo6p", - "sessionAttributes": {}, - "promptSessionAttributes": {}, - "inputText": "weather in london?" -} \ No newline at end of file From e5faf04239ecf486f2bd750799b3552b8852ce1e Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 May 2025 10:38:32 +0100 Subject: [PATCH 41/52] refactor: change access modifiers to internal for ParameterAccessor, ParameterMapper, ParameterTypeValidator, and ResultConverter; enhance error handling in ParameterAccessor --- README.md | 12 ++ .../{ => Helpers}/ParameterAccessor.cs | 23 ++- .../Helpers/ParameterMapper.cs | 2 +- .../Helpers/ParameterTypeValidator.cs | 2 +- .../Helpers/ResultConverter.cs | 2 +- ...rockAgentFunctionResolverExceptionTests.cs | 68 +++++++++ .../Helpers/ParameterAccessorTests.cs | 141 ++++++++++++++++++ 7 files changed, 242 insertions(+), 8 deletions(-) rename libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/{ => Helpers}/ParameterAccessor.cs (86%) create mode 100644 libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverExceptionTests.cs diff --git a/README.md b/README.md index 8dd862ad..5b7393ff 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,10 @@ Powertools for AWS Lambda (.NET) provides three core utilities: * **[Batch Processing](https://docs.powertools.aws.dev/lambda/dotnet/utilities/batch-processing/)** - The batch processing utility handles partial failures when processing batches from Amazon SQS, Amazon Kinesis Data Streams, and Amazon DynamoDB Streams. +* **[Event Handler AppSync Events](https://docs.powertools.aws.dev/lambda/dotnet/core/event_handler/appsync_events/)** - The event handler AppSync Events utility provides a simple way to handle AppSync events in your Lambda functions. It allows you to easily parse the event and access the data you need, without having to write complex code. + +* **[Event Handler Bedrock Agent Functions](https://docs.powertools.aws.dev/lambda/dotnet/core/event_handler/bedrock_agent_function/)** - The event handler Bedrock Agent Functions utility provides a simple way to handle Amazon Bedrock agent function events in your Lambda functions. It allows you to easily parse the event and access the data you need, without having to write complex code. + ### Installation The Powertools for AWS Lambda (.NET) utilities (.NET 6 and .NET 8) are available as NuGet packages. You can install the packages from [NuGet Gallery](https://www.nuget.org/packages?q=AWS+Lambda+Powertools*) or from Visual Studio editor by searching `AWS.Lambda.Powertools*` to see various utilities available. @@ -63,6 +67,14 @@ The Powertools for AWS Lambda (.NET) utilities (.NET 6 and .NET 8) are available `dotnet add package AWS.Lambda.Powertools.BatchProcessing` +* [AWS.Lambda.Powertools.EventHandler.AppSyncEvents](https://www.nuget.org/packages/AWS.Lambda.Powertools.EventHandler): + + `dotnet add package AWS.Lambda.Powertools.EventHandler` + +* [AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction](https://www.nuget.org/packages/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction): + + `dotnet add package AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction` + ## Examples We have provided examples focused specifically on each of the utilities. Each solution comes with an AWS Serverless Application Model (AWS SAM) templates to run your functions as a Zip package using the AWS Lambda .NET 6 or .NET 8 managed runtime; or as a container package using the AWS base images for .NET. diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterAccessor.cs similarity index 86% rename from libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs rename to libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterAccessor.cs index e81675ca..277905ea 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/ParameterAccessor.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterAccessor.cs @@ -15,13 +15,12 @@ using System.Globalization; -// ReSharper disable once CheckNamespace -namespace AWS.Lambda.Powertools.EventHandler.Resolvers; +namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers; /// /// Provides strongly-typed access to the parameters of an agent function call. /// -public class ParameterAccessor +internal class ParameterAccessor { private readonly List _parameters; @@ -74,7 +73,21 @@ public T GetOrDefault(string name, T defaultValue) return defaultValue; } - return ConvertParameter(parameter); + try + { + var result = ConvertParameter(parameter); + // If conversion returns default value but we have a non-null parameter, + // that means conversion failed, so return the provided default value + if (EqualityComparer.Default.Equals(result, default) && parameter.Value != null) + { + return defaultValue; + } + return result; + } + catch + { + return defaultValue; + } } private static T ConvertParameter(Parameter? parameter) @@ -138,4 +151,4 @@ private static T ConvertParameter(Parameter? parameter) // Return default for array and complex types return default!; } -} \ No newline at end of file +} diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs index 85cb1a3e..1c9edf77 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs @@ -23,7 +23,7 @@ namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Help /// /// Maps parameters for Bedrock Agent function handlers /// - public class ParameterMapper + internal class ParameterMapper { private readonly ParameterTypeValidator _validator = new(); diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterTypeValidator.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterTypeValidator.cs index 19123d49..164c4241 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterTypeValidator.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterTypeValidator.cs @@ -18,7 +18,7 @@ namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Help /// /// Validates parameter types for Bedrock Agent functions /// - public class ParameterTypeValidator + internal class ParameterTypeValidator { private static readonly HashSet BedrockParameterTypes = new() { diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs index ee668b28..72bbe383 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ResultConverter.cs @@ -22,7 +22,7 @@ namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Help /// /// Converts handler results to BedrockFunctionResponse /// - public class ResultConverter + internal class ResultConverter { /// /// Processes results from handler functions and converts to BedrockFunctionResponse diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverExceptionTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverExceptionTests.cs new file mode 100644 index 00000000..b05c3f42 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverExceptionTests.cs @@ -0,0 +1,68 @@ +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.EventHandler.Resolvers; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; + +namespace AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction +{ + public class BedrockAgentFunctionResolverExceptionTests + { + [Fact] + public void RegisterToolHandler_WithParameterMappingException_ReturnsErrorResponse() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + + // Register a tool that requires a complex parameter that can't be mapped automatically + resolver.Tool("ComplexTest", (TestComplexType complex) => $"Name: {complex.Name}"); + + var input = new BedrockFunctionRequest + { + Function = "ComplexTest", + Parameters = new List + { + // This parameter can't be automatically mapped to the complex type + new Parameter { Name = "complex", Value = "{\"name\":\"Test\"}", Type = "String" } + } + }; + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + // This should trigger the parameter mapping exception path + Assert.Contains("Error when invoking tool:", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void RegisterToolHandler_WithNestedExceptionInDelegateInvoke_HandlesCorrectly() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + + // Register a tool with a delegate that will throw an exception with inner exception + resolver.Tool("NestedExceptionTest", () => { + throw new AggregateException("Outer exception", + new ApplicationException("Inner exception message")); + return "Should not reach here"; + }); + + var input = new BedrockFunctionRequest { Function = "NestedExceptionTest" }; + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + // The error should contain the inner exception message + Assert.Contains("Inner exception message", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + // A test complex type that can't be automatically mapped from parameters + private class TestComplexType + { + public string Name { get; set; } = ""; + public int Value { get; set; } + } + } +} diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterAccessorTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterAccessorTests.cs index 34a8f246..b5d54046 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterAccessorTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/Helpers/ParameterAccessorTests.cs @@ -1,4 +1,5 @@ using AWS.Lambda.Powertools.EventHandler.Resolvers; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers; namespace AWS.Lambda.Powertools.EventHandler.BedrockAgentFunction.Helpers { @@ -193,5 +194,145 @@ public void Get_WithEmptyParameters_ReturnsDefault() // Assert Assert.Null(result); } + + [Fact] + public void GetAt_WithValidIndex_ReturnsValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "first", Value = "Value1", Type = "String" }, + new Parameter { Name = "second", Value = "42", Type = "Number" }, + new Parameter { Name = "third", Value = "true", Type = "Boolean" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var stringResult = accessor.GetAt(0); + var intResult = accessor.GetAt(1); + var boolResult = accessor.GetAt(2); + + // Assert + Assert.Equal("Value1", stringResult); + Assert.Equal(42, intResult); + Assert.True(boolResult); + } + + [Fact] + public void GetAt_WithInvalidIndex_ReturnsDefaultValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "param", Value = "Value", Type = "String" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var negativeIndexResult = accessor.GetAt(-1); + var tooLargeIndexResult = accessor.GetAt(1); + + // Assert + Assert.Null(negativeIndexResult); + Assert.Null(tooLargeIndexResult); + } + + [Fact] + public void GetAt_WithNullParameters_ReturnsDefaultValue() + { + // Arrange + var accessor = new ParameterAccessor(null); + + // Act + var result = accessor.GetAt(0); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetAt_WithNullValue_ReturnsDefaultValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "param", Value = null, Type = "String" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.GetAt(0); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetOrDefault_WithExistingParameter_ReturnsValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "name", Value = "TestValue", Type = "String" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.GetOrDefault("name", "DefaultValue"); + + // Assert + Assert.Equal("TestValue", result); + } + + [Fact] + public void GetOrDefault_WithNonExistentParameter_ReturnsDefaultValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "existing", Value = "value", Type = "String" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.GetOrDefault("nonExistent", "DefaultValue"); + + // Assert + Assert.Equal("DefaultValue", result); + } + + [Fact] + public void GetOrDefault_WithNullValue_ReturnsDefaultValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "param", Value = null, Type = "String" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.GetOrDefault("param", "DefaultValue"); + + // Assert + Assert.Equal("DefaultValue", result); + } + + [Fact] + public void GetOrDefault_WithInvalidConversion_ReturnsDefaultValue() + { + // Arrange + var parameters = new List + { + new Parameter { Name = "invalidNumber", Value = "not-a-number", Type = "Number" } + }; + var accessor = new ParameterAccessor(parameters); + + // Act + var result = accessor.GetOrDefault("invalidNumber", 999); + + // Assert + Assert.Equal(999, result); + } } } From 7906701481f18bf3a71036896c0a220163213a2c Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 May 2025 12:27:43 +0100 Subject: [PATCH 42/52] docs: update documentation for Bedrock Agent Function Resolver; enhance error handling and session attributes management --- .../event_handler/bedrock_agent_function.md | 216 ++++++++++++++---- ...dler.Resolvers.BedrockAgentFunction.csproj | 1 - .../Readme.md | 122 +++++++--- ...ockAgentFunctionResolverAdditionalTests.cs | 128 +++++++++++ 4 files changed, 394 insertions(+), 73 deletions(-) diff --git a/docs/core/event_handler/bedrock_agent_function.md b/docs/core/event_handler/bedrock_agent_function.md index 0b2b866e..2889406d 100644 --- a/docs/core/event_handler/bedrock_agent_function.md +++ b/docs/core/event_handler/bedrock_agent_function.md @@ -6,31 +6,71 @@ description: Event Handler - Bedrock Agent Function Resolver # 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](https://docs.aws.amazon.com/bedrock/latest/userguide/agents.html#agents-how) and focus on building your agent's logic without worrying about parsing and routing requests. + +```mermaid +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 - **Simple Tool Registration**: Register functions with descriptive names that Bedrock Agents can invoke - **Automatic Parameter Handling**: Parameters are automatically extracted from Bedrock Agent requests and converted to the appropriate types -- **Type Safety**: Strongly typed parameters and return values -- **Multiple Return Types**: Support for returning strings, primitive types, objects, or custom types -- **Flexible Input Options**: Support for various parameter types including string, int, bool, DateTime, and enums - **Lambda Context Access**: Easy access to Lambda context for logging and AWS Lambda features - **Dependency Injection Support**: Seamless integration with .NET's dependency injection system +- **AOT Compatibility**: 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: ```bash -dotnet add package AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver +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. + +??? note "Click to see example IaC templates" + ## Basic Usage -Here's a simple example showing how to register and use tool functions: +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. ```csharp using Amazon.BedrockAgentRuntime.Model; @@ -88,7 +128,7 @@ _resolver.Tool( ### Accessing Lambda Context -Access the Lambda context in your functions: +You can access to the original Lambda event or context for additional information. These are passed to the handler function as optional arguments. ```csharp _resolver.Tool( @@ -101,32 +141,85 @@ _resolver.Tool( }); ``` -### Working with Complex Return Types +### 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. -Return complex objects that will be converted to appropriate responses: +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. ```csharp -public class WeatherReport +resolver.Tool("CustomFailure", () => { - public string City { get; set; } - public string Conditions { get; set; } - public int Temperature { get; set; } - - public override string ToString() + // Return a custom FAILURE response + return new BedrockFunctionResponse { - return $"Weather in {City}: {Conditions}, {Temperature}°F"; + 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. + +```csharp +// 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(request.SessionAttributes ?? new Dictionary()) + { + ["counter"] = currentCount.ToString(), + ["lastAccessed"] = DateTime.UtcNow.ToString("o") + }; -_resolver.Tool( - "GetDetailedWeather", - "Returns detailed weather information for a location", - (string city) => new WeatherReport - { - City = city, - Conditions = "Partly Cloudy", - Temperature = 72 - }); + // 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 @@ -198,33 +291,70 @@ resolver.Tool( 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: - - Finds the registered handler for the requested function - - Extracts and converts parameters to the correct types - - Invokes your handler with the parameters - - Formats the response in the way Bedrock Agents expect + - Finds the registered handler for the requested function + - Extracts and converts parameters to the correct types + - Invokes your handler with the parameters + - Formats the response in the way Bedrock Agents expect 5. The agent receives the response and uses it to continue the conversation with the user ## Supported Parameter Types - `string` -- `int` / `long` -- `double` / `decimal` +- `int` +- `number` - `bool` -- `DateTime` -- `Guid` - `enum` types - `ILambdaContext` (for accessing Lambda context) - `ActionGroupInvocationInput` (for accessing raw request) - Any service registered in dependency injection -## Benefits -- **Reduced Boilerplate**: Eliminate repetitive code for parsing requests and formatting responses -- **Type Safety**: Strong typing for parameters and return values -- **Simplified Development**: Focus on business logic instead of request/response handling -- **Reusable Components**: Build a library of tool functions that can be shared across agents -- **Easy Testing**: Functions can be easily unit tested in isolation -- **Flexible Integration**: Works seamlessly with AWS Lambda and Bedrock Agents +## 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 + +```csharp +// 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: + +```csharp + +var services = new ServiceCollection(); +services.AddSingleton(); +services.AddBedrockResolver(); // Extension method to register the resolver + +var serviceProvider = services.BuildServiceProvider(); +var resolver = serviceProvider.GetRequiredService() + .RegisterTool(); // Register tools from the class during service registration + +``` ## Complete Example with Dependency Injection @@ -300,8 +430,4 @@ namespace MyBedrockAgent } } } -``` - -## Learn More - -For more information about Amazon Bedrock Agents and function integration, see the [Amazon Bedrock documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-tools.html). +``` \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj index 1f5c2aea..e6cbf62a 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.csproj @@ -9,7 +9,6 @@ enable enable true - 1.0.4 diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md index d1017d76..d58dce93 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md @@ -9,25 +9,40 @@ Amazon Bedrock Agents can invoke functions to perform tasks based on user input. - **Simple Tool Registration**: Register functions with descriptive names that Bedrock Agents can invoke - **Automatic Parameter Handling**: Parameters are automatically extracted from Bedrock Agent requests and converted to the appropriate types -- **Type Safety**: Strongly typed parameters and return values -- **Multiple Return Types**: Support for returning strings, primitive types, objects, or custom types -- **Flexible Input Options**: Support for various parameter types including string, int, bool, DateTime, and enums - **Lambda Context Access**: Easy access to Lambda context for logging and AWS Lambda features - **Dependency Injection Support**: Seamless integration with .NET's dependency injection system -- **Error Handling**: Automatic error capturing and formatting for responses -- **Async Support**: First-class support for asynchronous function execution +- **AOT Compatibility**: 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: ```bash -dotnet add package AWS.Lambda.Powertools.EventHandler.BedrockAgentFunctionResolver +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. + +??? note "Click to see example IaC templates" + ## Basic Usage -Here's a simple example showing how to register and use tool functions: +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. ```csharp using Amazon.BedrockAgentRuntime.Model; @@ -85,7 +100,7 @@ _resolver.Tool( ### Accessing Lambda Context -Access the Lambda context in your functions: +You can access to the original Lambda event or context for additional information. These are passed to the handler function as optional arguments. ```csharp _resolver.Tool( @@ -98,32 +113,85 @@ _resolver.Tool( }); ``` -### Working with Complex Return Types +### 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. -Return complex objects that will be converted to appropriate responses: +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. ```csharp -public class WeatherReport +resolver.Tool("CustomFailure", () => { - public string City { get; set; } - public string Conditions { get; set; } - public int Temperature { get; set; } - - public override string ToString() + // 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. + +```csharp +// 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)) { - return $"Weather in {City}: {Conditions}, {Temperature}°F"; + currentCount = count; } -} + + // Increment the counter + currentCount++; + + // Create a new dictionary with updated counter + var updatedSessionAttributes = new Dictionary(request.SessionAttributes ?? new Dictionary()) + { + ["counter"] = currentCount.ToString(), + ["lastAccessed"] = DateTime.UtcNow.ToString("o") + }; -_resolver.Tool( - "GetDetailedWeather", - "Returns detailed weather information for a location", - (string city) => new WeatherReport - { - City = city, - Conditions = "Partly Cloudy", - Temperature = 72 - }); + // 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 diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverAdditionalTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverAdditionalTests.cs index 62d09fde..3f73c686 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverAdditionalTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverAdditionalTests.cs @@ -200,6 +200,134 @@ public void Tool_WithBedrockFunctionResponseHandler_MapsCorrectly() // Assert Assert.Equal("Direct response", result.Response.FunctionResponse.ResponseBody.Text.Body); } + + [Fact] + public void Tool_WithCustomFailureResponse_ReturnsFailureState() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + 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 + } + } + }; + }); + + var input = new BedrockFunctionRequest { Function = "CustomFailure" }; + var context = new TestLambdaContext(); + + // Act + var result = resolver.Resolve(input, context); + + // Assert + Assert.Equal("Critical error occurred: Database unavailable", result.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("FAILURE", result.Response.FunctionResponse.ResponseState.ToString()); + } + + [Fact] + public void Tool_WithSessionAttributesPersistence_MaintainsStateAcrossInvocations() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + + // 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(request.SessionAttributes ?? new Dictionary()) + { + ["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 + }; + }); + + // First invocation - should start with 0 and increment to 1 + var firstInput = new BedrockFunctionRequest + { + Function = "CounterTool", + SessionAttributes = new Dictionary(), + PromptSessionAttributes = new Dictionary { ["prompt"] = "initial" } + }; + + // Second invocation - should use the session attributes from first response + var secondInput = new BedrockFunctionRequest { Function = "CounterTool" }; + + // Act + var firstResult = resolver.Resolve(firstInput); + // In a real scenario, the agent would pass the updated session attributes back to us + secondInput.SessionAttributes = firstResult.SessionAttributes; + secondInput.PromptSessionAttributes = firstResult.PromptSessionAttributes; + var secondResult = resolver.Resolve(secondInput); + + // Now a third invocation to verify the counter keeps incrementing + var thirdInput = new BedrockFunctionRequest { Function = "CounterTool" }; + thirdInput.SessionAttributes = secondResult.SessionAttributes; + thirdInput.PromptSessionAttributes = secondResult.PromptSessionAttributes; + var thirdResult = resolver.Resolve(thirdInput); + + // Assert + Assert.Equal("Current count: 1", firstResult.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("Current count: 2", secondResult.Response.FunctionResponse.ResponseBody.Text.Body); + Assert.Equal("Current count: 3", thirdResult.Response.FunctionResponse.ResponseBody.Text.Body); + + // Verify session attributes are maintained + Assert.Equal("1", firstResult.SessionAttributes["counter"]); + Assert.Equal("2", secondResult.SessionAttributes["counter"]); + Assert.Equal("3", thirdResult.SessionAttributes["counter"]); + + // Verify prompt attributes are preserved + Assert.Equal("initial", firstResult.PromptSessionAttributes["prompt"]); + Assert.Equal("initial", secondResult.PromptSessionAttributes["prompt"]); + Assert.Equal("initial", thirdResult.PromptSessionAttributes["prompt"]); + } private class TestObject { From 913f1c80c0cf372422b2d1df4c5f1a027eb7b43a Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 May 2025 11:31:35 +0100 Subject: [PATCH 43/52] Update version.json Signed-off-by: Henrique Graca <999396+hjgraca@users.noreply.github.com> --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index cb5c98bc..53805977 100644 --- a/version.json +++ b/version.json @@ -10,6 +10,6 @@ "Idempotency": "1.3.0", "BatchProcessing": "1.2.1", "EventHandler": "1.0.0", - "BedrockAgentFunctionResolver": "1.0.0-alpha.1", + "EventHandler.Resolvers.BedrockAgentFunction": "1.0.0-alpha.1", } } From c03ffb368aa65535e20b32da8cd8f9eecb066673 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 May 2025 12:29:01 +0100 Subject: [PATCH 44/52] chore: update BedrockAgentFunction version to 1.0.0-alpha.2 in version.json --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 53805977..92675f91 100644 --- a/version.json +++ b/version.json @@ -10,6 +10,6 @@ "Idempotency": "1.3.0", "BatchProcessing": "1.2.1", "EventHandler": "1.0.0", - "EventHandler.Resolvers.BedrockAgentFunction": "1.0.0-alpha.1", + "EventHandler.Resolvers.BedrockAgentFunction": "1.0.0-alpha.2" } } From ec642af5d4c091789c3ef3a9cbd477eb0633c260 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 2 Jun 2025 09:59:25 +0100 Subject: [PATCH 45/52] docs: update documentation for Bedrock Agent Function Resolver; add TODO for CDK integration and remove redundant resource requirements --- docs/core/event_handler/bedrock_agent_function.md | 3 +++ .../Readme.md | 6 ------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/core/event_handler/bedrock_agent_function.md b/docs/core/event_handler/bedrock_agent_function.md index 2889406d..f9acae45 100644 --- a/docs/core/event_handler/bedrock_agent_function.md +++ b/docs/core/event_handler/bedrock_agent_function.md @@ -68,6 +68,9 @@ You must create an Amazon Bedrock Agent with at least one action group. Each act ??? note "Click to see example IaC templates" +TODO: add cdk + + ## 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. diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md index d58dce93..9904f64e 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Readme.md @@ -34,12 +34,6 @@ Install the package via NuGet: 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. - -??? note "Click to see example IaC templates" - ## 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. From a38c30ec71bd8059e3f222b190836ecc49494418 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:51:43 +0100 Subject: [PATCH 46/52] feat: enhance BedrockAgentFunctionResolver with custom type support and JSON serialization options --- .../BedrockAgentFunctionResolver.cs | 12 ++- .../BedrockAgentFunctionResolverExtensions.cs | 19 +++-- .../DiBedrockAgentFunctionResolver.cs | 6 +- .../Helpers/ParameterMapper.cs | 79 ++++++++++++++++--- .../BedrockAgentFunctionResolverTests.cs | 68 ++++++++++++++++ 5 files changed, 164 insertions(+), 20 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs index a4ece182..d24cf62c 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs @@ -13,8 +13,7 @@ * permissions and limitations under the License. */ -using System.Globalization; -using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using Amazon.Lambda.Core; using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Helpers; @@ -48,6 +47,15 @@ private readonly private readonly ResultConverter _resultConverter = new(); private readonly ParameterMapper _parameterMapper = new(); + /// + /// Initializes a new instance of the class. + /// Optionally accepts a type resolver for JSON serialization. + /// + public BedrockAgentFunctionResolver(IJsonTypeInfoResolver? typeResolver = null) + { + _parameterMapper = new ParameterMapper(typeResolver); + } + /// /// Checks if another tool can be registered, and logs a warning if the maximum limit is reached /// or if a tool with the same name is already registered diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs index bd2c8cbc..307152a3 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolverExtensions.cs @@ -16,6 +16,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reflection; +using System.Text.Json.Serialization.Metadata; using Microsoft.Extensions.DependencyInjection; // ReSharper disable once CheckNamespace @@ -30,6 +31,7 @@ public static class BedrockResolverExtensions /// Registers a Bedrock Agent Function Resolver with dependency injection support. /// /// The service collection to add the resolver to. + /// /// The updated service collection. /// /// @@ -41,10 +43,12 @@ public static class BedrockResolverExtensions /// } /// /// - public static IServiceCollection AddBedrockResolver(this IServiceCollection services) + public static IServiceCollection AddBedrockResolver( + this IServiceCollection services, + IJsonTypeInfoResolver? typeResolver = null) { services.AddSingleton(sp => - new DiBedrockAgentFunctionResolver(sp)); + new DiBedrockAgentFunctionResolver(sp, typeResolver)); return services; } @@ -72,7 +76,8 @@ public static IServiceCollection AddBedrockResolver(this IServiceCollection serv /// resolver.RegisterTool<WeatherTools>(); /// /// - public static BedrockAgentFunctionResolver RegisterTool<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>( + public static BedrockAgentFunctionResolver RegisterTool< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>( this BedrockAgentFunctionResolver resolver) where T : class { @@ -89,14 +94,14 @@ public static IServiceCollection AddBedrockResolver(this IServiceCollection serv if (attr == null) continue; string toolName = attr.Name ?? method.Name; - string description = attr.Description ?? + string description = attr.Description ?? string.Empty; // Create delegate from the static method var del = Delegate.CreateDelegate( - GetDelegateType(method), + GetDelegateType(method), method); - + // Call the Tool method directly instead of using reflection resolver.Tool(toolName, description, del); } @@ -109,7 +114,7 @@ private static Type GetDelegateType(MethodInfo method) var parameters = method.GetParameters(); var parameterTypes = parameters.Select(p => p.ParameterType).ToList(); parameterTypes.Add(method.ReturnType); - + return Expression.GetDelegateType(parameterTypes.ToArray()); } } diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/DiBedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/DiBedrockAgentFunctionResolver.cs index 0d893623..82064d43 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/DiBedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/DiBedrockAgentFunctionResolver.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization.Metadata; + namespace AWS.Lambda.Powertools.EventHandler.Resolvers; /// @@ -14,7 +16,9 @@ internal class DiBedrockAgentFunctionResolver : BedrockAgentFunctionResolver /// Initializes a new instance of the class. /// /// The service provider for dependency injection. - public DiBedrockAgentFunctionResolver(IServiceProvider serviceProvider) + /// + public DiBedrockAgentFunctionResolver(IServiceProvider serviceProvider, IJsonTypeInfoResolver? typeResolver = null) + : base(typeResolver) { ServiceProvider = serviceProvider; } diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs index 1c9edf77..1f723d98 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/Helpers/ParameterMapper.cs @@ -15,6 +15,7 @@ using System.Reflection; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using Amazon.Lambda.Core; using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; @@ -26,7 +27,13 @@ namespace AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Help internal class ParameterMapper { private readonly ParameterTypeValidator _validator = new(); - + private readonly IJsonTypeInfoResolver? _typeResolver; + + public ParameterMapper(IJsonTypeInfoResolver? typeResolver = null) + { + _typeResolver = typeResolver; + } + /// /// Maps parameters for a handler method from a Bedrock function request /// @@ -36,15 +43,14 @@ internal class ParameterMapper /// Optional service provider for dependency injection /// Array of arguments to pass to the handler public object?[] MapParameters( - MethodInfo methodInfo, - BedrockFunctionRequest input, + MethodInfo methodInfo, + BedrockFunctionRequest input, ILambdaContext? context, IServiceProvider? serviceProvider) { var parameters = methodInfo.GetParameters(); var args = new object?[parameters.Length]; var accessor = new ParameterAccessor(input.Parameters); - var bedrockParamIndex = 0; for (var i = 0; i < parameters.Length; i++) { @@ -54,15 +60,66 @@ internal class ParameterMapper if (paramType == typeof(ILambdaContext)) { args[i] = context; + continue; // Skip further processing for this parameter } else if (paramType == typeof(BedrockFunctionRequest)) { args[i] = input; + continue; // Skip further processing for this parameter + } + + // Try to deserialize custom complex type from InputText + if (!string.IsNullOrEmpty(input.InputText) && + !paramType.IsPrimitive && + paramType != typeof(string) && + !paramType.IsEnum) + { + try + { + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + if (_typeResolver != null) + { + options.TypeInfoResolver = _typeResolver; + + // Get the JsonTypeInfo for the parameter type + var jsonTypeInfo = _typeResolver.GetTypeInfo(paramType, options); + if (jsonTypeInfo != null) + { + // Use the AOT-friendly overload with JsonTypeInfo + args[i] = JsonSerializer.Deserialize(input.InputText, jsonTypeInfo); + + if (args[i] != null) + { + continue; + } + } + } + else + { + // Fallback to non-AOT deserialization with warning +#pragma warning disable IL2026, IL3050 + args[i] = JsonSerializer.Deserialize(input.InputText, paramType, options); +#pragma warning restore IL2026, IL3050 + + if (args[i] != null) + { + continue; + } + } + } + catch + { + // Deserialization failed, continue to regular parameter mapping + } } - else if (_validator.IsBedrockParameter(paramType)) + + if (_validator.IsBedrockParameter(paramType)) { - args[i] = MapBedrockParameter(paramType, parameter.Name ?? $"arg{bedrockParamIndex}", accessor); - bedrockParamIndex++; + args[i] = MapBedrockParameter(paramType, parameter.Name ?? $"arg{i}", accessor); } else if (serviceProvider != null) { @@ -107,9 +164,11 @@ internal class ParameterMapper if (paramType == typeof(double[])) return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.DoubleArray); if (paramType == typeof(bool[])) - return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.BooleanArray); + return JsonSerializer.Deserialize(jsonArrayStr, + BedrockFunctionResolverContext.Default.BooleanArray); if (paramType == typeof(decimal[])) - return JsonSerializer.Deserialize(jsonArrayStr, BedrockFunctionResolverContext.Default.DecimalArray); + return JsonSerializer.Deserialize(jsonArrayStr, + BedrockFunctionResolverContext.Default.DecimalArray); } catch (JsonException) { @@ -147,4 +206,4 @@ internal class ParameterMapper return null; } } -} +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverTests.cs b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverTests.cs index 5e630214..1090a248 100644 --- a/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.EventHandler.Tests/BedrockAgentFunction/BedrockAgentFunctionResolverTests.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Text; +using System.Text.Json.Serialization; using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.EventHandler.Resolvers; @@ -800,6 +801,62 @@ public void TestToolOverrideWithWarning() Assert.Equal("New Calculator", result.Response.FunctionResponse.ResponseBody.Text.Body); } + [Fact] + public void TestFunctionHandlerWithCustomType() + { + // Arrange + var resolver = new BedrockAgentFunctionResolver(); + 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)}"; + } + ); + + var input = new BedrockFunctionRequest + { + Function = "PriceCalculator", + InputText = "{\"Price\": 29.99}", // JSON representation of MyCustomType + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains("35.99", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + + [Fact] + public void TestFunctionHandlerWithCustomTypeWithTypeInfoResolver() + { + // Arrange + 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)}"; + } + ); + + var input = new BedrockFunctionRequest + { + Function = "PriceCalculator", + InputText = "{\"Price\": 29.99}", // JSON representation of MyCustomType + }; + + // Act + var result = resolver.Resolve(input); + + // Assert + Assert.Contains("35.99", result.Response.FunctionResponse.ResponseBody.Text.Body); + } + [Fact] public void TestAttributeBasedToolRegistration() { @@ -873,3 +930,14 @@ public async Task DoSomething(string location, int days) return await Task.FromResult($"Forecast for {location} for {days} days"); } } + +public class MyCustomType +{ + public decimal Price { get; set; } +} + + +[JsonSerializable(typeof(MyCustomType))] +public partial class MycustomSerializationContext : JsonSerializerContext +{ +} \ No newline at end of file From cc0b5a288a1ed6e412776110876d8d70a326b468 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Mon, 2 Jun 2025 18:16:23 +0100 Subject: [PATCH 47/52] fix: initialize ParameterMapper in BedrockAgentFunctionResolver to ensure proper instantiation --- .../BedrockAgentFunctionResolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs index d24cf62c..d0e15c09 100644 --- a/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs +++ b/libraries/src/AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction/BedrockAgentFunctionResolver.cs @@ -45,7 +45,7 @@ private readonly private readonly ParameterTypeValidator _parameterValidator = new(); private readonly ResultConverter _resultConverter = new(); - private readonly ParameterMapper _parameterMapper = new(); + private readonly ParameterMapper _parameterMapper; /// /// Initializes a new instance of the class. From 3fec46888b56ebdf51f8b63f400dd18b2e418652 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Tue, 3 Jun 2025 11:37:10 +0100 Subject: [PATCH 48/52] add examples --- .../BedrockAgentFunction/infra/.gitignore | 8 + .../BedrockAgentFunction/infra/.npmignore | 6 + .../BedrockAgentFunction/infra/README.md | 14 + .../BedrockAgentFunction/infra/cdk.json | 96 + .../BedrockAgentFunction/infra/jest.config.js | 8 + .../infra/lib/bedrockagents-stack.ts | 122 + .../infra/package-lock.json | 4448 +++++++++++++++++ .../BedrockAgentFunction/infra/package.json | 26 + .../BedrockAgentFunction/infra/tsconfig.json | 31 + .../src/AirportService.cs | 222 + .../src/BedrockAgentFunction.csproj | 22 + .../BedrockAgentFunction/src/Function.cs | 55 + .../BedrockAgentFunction/src/Readme.md | 47 + .../src/aws-lambda-tools-defaults.json | 15 + 14 files changed, 5120 insertions(+) create mode 100644 examples/Event Handler/BedrockAgentFunction/infra/.gitignore create mode 100644 examples/Event Handler/BedrockAgentFunction/infra/.npmignore create mode 100644 examples/Event Handler/BedrockAgentFunction/infra/README.md create mode 100644 examples/Event Handler/BedrockAgentFunction/infra/cdk.json create mode 100644 examples/Event Handler/BedrockAgentFunction/infra/jest.config.js create mode 100644 examples/Event Handler/BedrockAgentFunction/infra/lib/bedrockagents-stack.ts create mode 100644 examples/Event Handler/BedrockAgentFunction/infra/package-lock.json create mode 100644 examples/Event Handler/BedrockAgentFunction/infra/package.json create mode 100644 examples/Event Handler/BedrockAgentFunction/infra/tsconfig.json create mode 100644 examples/Event Handler/BedrockAgentFunction/src/AirportService.cs create mode 100644 examples/Event Handler/BedrockAgentFunction/src/BedrockAgentFunction.csproj create mode 100644 examples/Event Handler/BedrockAgentFunction/src/Function.cs create mode 100644 examples/Event Handler/BedrockAgentFunction/src/Readme.md create mode 100644 examples/Event Handler/BedrockAgentFunction/src/aws-lambda-tools-defaults.json diff --git a/examples/Event Handler/BedrockAgentFunction/infra/.gitignore b/examples/Event Handler/BedrockAgentFunction/infra/.gitignore new file mode 100644 index 00000000..f60797b6 --- /dev/null +++ b/examples/Event Handler/BedrockAgentFunction/infra/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/examples/Event Handler/BedrockAgentFunction/infra/.npmignore b/examples/Event Handler/BedrockAgentFunction/infra/.npmignore new file mode 100644 index 00000000..c1d6d45d --- /dev/null +++ b/examples/Event Handler/BedrockAgentFunction/infra/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/examples/Event Handler/BedrockAgentFunction/infra/README.md b/examples/Event Handler/BedrockAgentFunction/infra/README.md new file mode 100644 index 00000000..9315fe5b --- /dev/null +++ b/examples/Event Handler/BedrockAgentFunction/infra/README.md @@ -0,0 +1,14 @@ +# Welcome to your CDK TypeScript project + +This is a blank project for CDK development with TypeScript. + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +## Useful commands + +* `npm run build` compile typescript to js +* `npm run watch` watch for changes and compile +* `npm run test` perform the jest unit tests +* `npx cdk deploy` deploy this stack to your default AWS account/region +* `npx cdk diff` compare deployed stack with current state +* `npx cdk synth` emits the synthesized CloudFormation template diff --git a/examples/Event Handler/BedrockAgentFunction/infra/cdk.json b/examples/Event Handler/BedrockAgentFunction/infra/cdk.json new file mode 100644 index 00000000..eea31fee --- /dev/null +++ b/examples/Event Handler/BedrockAgentFunction/infra/cdk.json @@ -0,0 +1,96 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/infra.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, + "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, + "@aws-cdk/core:enableAdditionalMetadataCollection": true, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false, + "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, + "@aws-cdk/aws-events:requireEventBusPolicySid": true, + "@aws-cdk/core:aspectPrioritiesMutating": true, + "@aws-cdk/aws-dynamodb:retainTableReplica": true, + "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true, + "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true, + "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true, + "@aws-cdk/aws-s3:publicAccessBlockedByDefault": true + } +} diff --git a/examples/Event Handler/BedrockAgentFunction/infra/jest.config.js b/examples/Event Handler/BedrockAgentFunction/infra/jest.config.js new file mode 100644 index 00000000..08263b89 --- /dev/null +++ b/examples/Event Handler/BedrockAgentFunction/infra/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/examples/Event Handler/BedrockAgentFunction/infra/lib/bedrockagents-stack.ts b/examples/Event Handler/BedrockAgentFunction/infra/lib/bedrockagents-stack.ts new file mode 100644 index 00000000..bf08fb81 --- /dev/null +++ b/examples/Event Handler/BedrockAgentFunction/infra/lib/bedrockagents-stack.ts @@ -0,0 +1,122 @@ +import { + Stack, + type StackProps, + CfnOutput, + RemovalPolicy, + Arn, + Duration, +} from 'aws-cdk-lib'; +import type { Construct } from 'constructs'; +import { Runtime, Function, Code, Architecture } from 'aws-cdk-lib/aws-lambda'; +import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { CfnAgent } from 'aws-cdk-lib/aws-bedrock'; +import { + Effect, + PolicyDocument, + PolicyStatement, + Role, + ServicePrincipal, +} from 'aws-cdk-lib/aws-iam'; + +export class BedrockAgentsStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const fnName = 'BedrockAgentsFn'; + const logGroup = new LogGroup(this, 'MyLogGroup', { + logGroupName: `/aws/lambda/${fnName}`, + removalPolicy: RemovalPolicy.DESTROY, + retention: RetentionDays.ONE_DAY, + }); + + const fn = new Function(this, 'MyFunction', { + functionName: fnName, + logGroup, + timeout: Duration.minutes(3), + runtime: Runtime.DOTNET_8, + handler: 'BedrockAgentFunction', + code: Code.fromAsset('../release/BedrockAgentFunction.zip'), + architecture: Architecture.X86_64, + }); + + const agentRole = new Role(this, 'MyAgentRole', { + assumedBy: new ServicePrincipal('bedrock.amazonaws.com'), + description: 'Role for Bedrock airport agent', + inlinePolicies: { + bedrock: new PolicyDocument({ + statements: [ + new PolicyStatement({ + actions: [ + 'bedrock:*', + ], + resources: [ + Arn.format( + { + service: 'bedrock', + resource: 'foundation-model/*', + region: 'us-*', + account: '', + }, + Stack.of(this) + ), + Arn.format( + { + service: 'bedrock', + resource: 'inference-profile/*', + region: 'us-*', + account: '*', + }, + Stack.of(this) + ), + ], + }), + ], + }), + }, + }); + + const agent = new CfnAgent(this, 'MyCfnAgent', { + agentName: 'airportAgent', + actionGroups: [ + { + actionGroupName: 'airportActionGroup', + actionGroupExecutor: { + lambda: fn.functionArn, + }, + functionSchema: { + functions: [ + { + name: 'getAirportCodeForCity', + description: 'Get airport code and full airport name for a specific city', + parameters: { + city: { + type: 'string', + description: 'The name of the city to get the airport code for', + required: true, + }, + }, + }, + ], + }, + }, + ], + agentResourceRoleArn: agentRole.roleArn, + autoPrepare: true, + description: 'A simple airport agent', + foundationModel: `arn:aws:bedrock:us-west-2:${Stack.of(this).account}: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 and airport full name for that city.', + }); + + fn.addPermission('BedrockAgentInvokePermission', { + principal: new ServicePrincipal('bedrock.amazonaws.com'), + action: 'lambda:InvokeFunction', + sourceAccount: this.account, + sourceArn: `arn:aws:bedrock:${this.region}:${this.account}:agent/${agent.attrAgentId}`, + }); + + new CfnOutput(this, 'FunctionArn', { + value: fn.functionArn, + }); + } +} diff --git a/examples/Event Handler/BedrockAgentFunction/infra/package-lock.json b/examples/Event Handler/BedrockAgentFunction/infra/package-lock.json new file mode 100644 index 00000000..cb3ffa66 --- /dev/null +++ b/examples/Event Handler/BedrockAgentFunction/infra/package-lock.json @@ -0,0 +1,4448 @@ +{ + "name": "infra", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "infra", + "version": "0.1.0", + "dependencies": { + "aws-cdk-lib": "2.198.0", + "constructs": "^10.0.0" + }, + "bin": { + "infra": "bin/infra.js" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "22.7.9", + "aws-cdk": "2.1017.1", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.237", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.237.tgz", + "integrity": "sha512-OlXylbXI52lboFVJBFLae+WB99qWmI121x/wXQHEMj2RaVNVbWE+OAHcDk2Um1BitUQCaTf9ki57B0Fuqx0Rvw==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "41.2.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-41.2.0.tgz", + "integrity": "sha512-JaulVS6z9y5+u4jNmoWbHZRs9uGOnmn/ktXygNWKNu1k6lF3ad4so3s18eRu15XCbUIomxN9WPYT6Ehh7hzONw==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 14.15.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.1", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.3.tgz", + "integrity": "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz", + "integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.3", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz", + "integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.4.tgz", + "integrity": "sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", + "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "22.7.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", + "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/aws-cdk": { + "version": "2.1017.1", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1017.1.tgz", + "integrity": "sha512-KtDdkMhfVjDeexjpMrVoSlz2mTYI5BE/KotvJ7iFbZy1G0nkpW1ImZ54TdBefeeFmZ+8DAjU3I6nUFtymyOI1A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 14.15.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.198.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.198.0.tgz", + "integrity": "sha512-CyZ+lnRsCsLskzQLPO0EiGl5EVcLluhfa67df3b8/gJfsm+91SHJa75OH+ymdGtUp5Vn/MWUPsujw0EhWMfsIQ==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "2.2.237", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^41.2.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.0", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.7.2", + "table": "^6.9.0", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.17.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.11", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.0.6", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.2", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001720", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001720.tgz", + "integrity": "sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/constructs": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", + "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", + "license": "Apache-2.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.161", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.161.tgz", + "integrity": "sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.3.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz", + "integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/examples/Event Handler/BedrockAgentFunction/infra/package.json b/examples/Event Handler/BedrockAgentFunction/infra/package.json new file mode 100644 index 00000000..eb6545ca --- /dev/null +++ b/examples/Event Handler/BedrockAgentFunction/infra/package.json @@ -0,0 +1,26 @@ +{ + "name": "infra", + "version": "0.1.0", + "bin": { + "infra": "bin/infra.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "22.7.9", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "aws-cdk": "2.1017.1", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + }, + "dependencies": { + "aws-cdk-lib": "2.198.0", + "constructs": "^10.0.0" + } +} diff --git a/examples/Event Handler/BedrockAgentFunction/infra/tsconfig.json b/examples/Event Handler/BedrockAgentFunction/infra/tsconfig.json new file mode 100644 index 00000000..28bb557f --- /dev/null +++ b/examples/Event Handler/BedrockAgentFunction/infra/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "es2022" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} diff --git a/examples/Event Handler/BedrockAgentFunction/src/AirportService.cs b/examples/Event Handler/BedrockAgentFunction/src/AirportService.cs new file mode 100644 index 00000000..aa26e7f9 --- /dev/null +++ b/examples/Event Handler/BedrockAgentFunction/src/AirportService.cs @@ -0,0 +1,222 @@ +namespace BedrockAgentFunction; + +public class AirportService +{ + private readonly Dictionary _airportsByCity = new(StringComparer.OrdinalIgnoreCase) + { + { + "New York", + new AirportInfo { City = "New York", Code = "JFK", Name = "John F. Kennedy International Airport" } + }, + { "London", new AirportInfo { City = "London", Code = "LHR", Name = "London Heathrow Airport" } }, + { "Paris", new AirportInfo { City = "Paris", Code = "CDG", Name = "Charles de Gaulle Airport" } }, + { "Tokyo", new AirportInfo { City = "Tokyo", Code = "HND", Name = "Tokyo Haneda Airport" } }, + { "Sydney", new AirportInfo { City = "Sydney", Code = "SYD", Name = "Sydney Airport" } }, + { + "Los Angeles", + new AirportInfo { City = "Los Angeles", Code = "LAX", Name = "Los Angeles International Airport" } + }, + { "Berlin", new AirportInfo { City = "Berlin", Code = "TXL", Name = "Berlin Tegel Airport" } }, + { "Dubai", new AirportInfo { City = "Dubai", Code = "DXB", Name = "Dubai International Airport" } }, + { + "Toronto", + new AirportInfo { City = "Toronto", Code = "YYZ", Name = "Toronto Pearson International Airport" } + }, + { "Singapore", new AirportInfo { City = "Singapore", Code = "SIN", Name = "Singapore Changi Airport" } }, + { "Hong Kong", new AirportInfo { City = "Hong Kong", Code = "HKG", Name = "Hong Kong International Airport" } }, + { "Madrid", new AirportInfo { City = "Madrid", Code = "MAD", Name = "Adolfo Suárez Madrid–Barajas Airport" } }, + { "Rome", new AirportInfo { City = "Rome", Code = "FCO", Name = "Leonardo da Vinci International Airport" } }, + { "Moscow", new AirportInfo { City = "Moscow", Code = "SVO", Name = "Sheremetyevo International Airport" } }, + { + "São Paulo", + new AirportInfo + { + City = "São Paulo", Code = "GRU", + Name = "São Paulo/Guarulhos–Governador André Franco Montoro International Airport" + } + }, + { "Istanbul", new AirportInfo { City = "Istanbul", Code = "IST", Name = "Istanbul Airport" } }, + { "Bangkok", new AirportInfo { City = "Bangkok", Code = "BKK", Name = "Suvarnabhumi Airport" } }, + { + "Mexico City", + new AirportInfo { City = "Mexico City", Code = "MEX", Name = "Mexico City International Airport" } + }, + { "Cairo", new AirportInfo { City = "Cairo", Code = "CAI", Name = "Cairo International Airport" } }, + { + "Buenos Aires", + new AirportInfo { City = "Buenos Aires", Code = "EZE", Name = "Ministro Pistarini International Airport" } + }, + { + "Kuala Lumpur", + new AirportInfo { City = "Kuala Lumpur", Code = "KUL", Name = "Kuala Lumpur International Airport" } + }, + { "Amsterdam", new AirportInfo { City = "Amsterdam", Code = "AMS", Name = "Amsterdam Airport Schiphol" } }, + { "Barcelona", new AirportInfo { City = "Barcelona", Code = "BCN", Name = "Barcelona–El Prat Airport" } }, + { "Lima", new AirportInfo { City = "Lima", Code = "LIM", Name = "Jorge Chávez International Airport" } }, + { "Seoul", new AirportInfo { City = "Seoul", Code = "ICN", Name = "Incheon International Airport" } }, + { + "Rio de Janeiro", + new AirportInfo + { + City = "Rio de Janeiro", Code = "GIG", + Name = "Rio de Janeiro/Galeão–Antonio Carlos Jobim International Airport" + } + }, + { "Dublin", new AirportInfo { City = "Dublin", Code = "DUB", Name = "Dublin Airport" } }, + { "Brussels", new AirportInfo { City = "Brussels", Code = "BRU", Name = "Brussels Airport" } }, + { "Lisbon", new AirportInfo { City = "Lisbon", Code = "LIS", Name = "Lisbon Portela Airport" } }, + { "Athens", new AirportInfo { City = "Athens", Code = "ATH", Name = "Athens International Airport" } }, + { "Oslo", new AirportInfo { City = "Oslo", Code = "OSL", Name = "Oslo Airport, Gardermoen" } }, + { "Stockholm", new AirportInfo { City = "Stockholm", Code = "ARN", Name = "Stockholm Arlanda Airport" } }, + { "Helsinki", new AirportInfo { City = "Helsinki", Code = "HEL", Name = "Helsinki-Vantaa Airport" } }, + { "Prague", new AirportInfo { City = "Prague", Code = "PRG", Name = "Václav Havel Airport Prague" } }, + { "Warsaw", new AirportInfo { City = "Warsaw", Code = "WAW", Name = "Warsaw Chopin Airport" } }, + { "Copenhagen", new AirportInfo { City = "Copenhagen", Code = "CPH", Name = "Copenhagen Airport" } }, + { + "Budapest", + new AirportInfo { City = "Budapest", Code = "BUD", Name = "Budapest Ferenc Liszt International Airport" } + }, + { "Osaka", new AirportInfo { City = "Osaka", Code = "KIX", Name = "Kansai International Airport" } }, + { + "San Francisco", + new AirportInfo { City = "San Francisco", Code = "SFO", Name = "San Francisco International Airport" } + }, + { "Miami", new AirportInfo { City = "Miami", Code = "MIA", Name = "Miami International Airport" } }, + { + "Seattle", new AirportInfo { City = "Seattle", Code = "SEA", Name = "Seattle–Tacoma International Airport" } + }, + { "Vancouver", new AirportInfo { City = "Vancouver", Code = "YVR", Name = "Vancouver International Airport" } }, + { "Melbourne", new AirportInfo { City = "Melbourne", Code = "MEL", Name = "Melbourne Airport" } }, + { "Auckland", new AirportInfo { City = "Auckland", Code = "AKL", Name = "Auckland Airport" } }, + { "Doha", new AirportInfo { City = "Doha", Code = "DOH", Name = "Hamad International Airport" } }, + { + "Kuwait City", new AirportInfo { City = "Kuwait City", Code = "KWI", Name = "Kuwait International Airport" } + }, + { + "Bangalore", new AirportInfo { City = "Bangalore", Code = "BLR", Name = "Kempegowda International Airport" } + }, + { + "Beijing", + new AirportInfo { City = "Beijing", Code = "PEK", Name = "Beijing Capital International Airport" } + }, + { + "Shanghai", + new AirportInfo { City = "Shanghai", Code = "PVG", Name = "Shanghai Pudong International Airport" } + }, + { "Manila", new AirportInfo { City = "Manila", Code = "MNL", Name = "Ninoy Aquino International Airport" } }, + { + "Jakarta", new AirportInfo { City = "Jakarta", Code = "CGK", Name = "Soekarno–Hatta International Airport" } + }, + { + "Santiago", + new AirportInfo + { City = "Santiago", Code = "SCL", Name = "Comodoro Arturo Merino Benítez International Airport" } + }, + { "Lagos", new AirportInfo { City = "Lagos", Code = "LOS", Name = "Murtala Muhammed International Airport" } }, + { "Nairobi", new AirportInfo { City = "Nairobi", Code = "NBO", Name = "Jomo Kenyatta International Airport" } }, + { "Chicago", new AirportInfo { City = "Chicago", Code = "ORD", Name = "O'Hare International Airport" } }, + { + "Atlanta", + new AirportInfo + { City = "Atlanta", Code = "ATL", Name = "Hartsfield–Jackson Atlanta International Airport" } + }, + { + "Dallas", + new AirportInfo { City = "Dallas", Code = "DFW", Name = "Dallas/Fort Worth International Airport" } + }, + { + "Washington, D.C.", + new AirportInfo + { City = "Washington, D.C.", Code = "IAD", Name = "Washington Dulles International Airport" } + }, + { "Boston", new AirportInfo { City = "Boston", Code = "BOS", Name = "Logan International Airport" } }, + { + "Philadelphia", + new AirportInfo { City = "Philadelphia", Code = "PHL", Name = "Philadelphia International Airport" } + }, + { "Orlando", new AirportInfo { City = "Orlando", Code = "MCO", Name = "Orlando International Airport" } }, + { "Denver", new AirportInfo { City = "Denver", Code = "DEN", Name = "Denver International Airport" } }, + { + "Phoenix", + new AirportInfo { City = "Phoenix", Code = "PHX", Name = "Phoenix Sky Harbor International Airport" } + }, + { "Las Vegas", new AirportInfo { City = "Las Vegas", Code = "LAS", Name = "McCarran International Airport" } }, + { + "Houston", new AirportInfo { City = "Houston", Code = "IAH", Name = "George Bush Intercontinental Airport" } + }, + { + "Detroit", + new AirportInfo { City = "Detroit", Code = "DTW", Name = "Detroit Metropolitan Wayne County Airport" } + }, + { + "Charlotte", + new AirportInfo { City = "Charlotte", Code = "CLT", Name = "Charlotte Douglas International Airport" } + }, + { + "Baltimore", + new AirportInfo + { + City = "Baltimore", Code = "BWI", Name = "Baltimore/Washington International Thurgood Marshall Airport" + } + }, + { + "Minneapolis", + new AirportInfo + { City = "Minneapolis", Code = "MSP", Name = "Minneapolis–Saint Paul International Airport" } + }, + { "San Diego", new AirportInfo { City = "San Diego", Code = "SAN", Name = "San Diego International Airport" } }, + { "Portland", new AirportInfo { City = "Portland", Code = "PDX", Name = "Portland International Airport" } }, + { + "Salt Lake City", + new AirportInfo { City = "Salt Lake City", Code = "SLC", Name = "Salt Lake City International Airport" } + }, + { + "Cincinnati", + new AirportInfo + { City = "Cincinnati", Code = "CVG", Name = "Cincinnati/Northern Kentucky International Airport" } + }, + { + "St. Louis", + new AirportInfo { City = "St. Louis", Code = "STL", Name = "St. Louis Lambert International Airport" } + }, + { + "Indianapolis", + new AirportInfo { City = "Indianapolis", Code = "IND", Name = "Indianapolis International Airport" } + }, + { "Tampa", new AirportInfo { City = "Tampa", Code = "TPA", Name = "Tampa International Airport" } }, + { "Milan", new AirportInfo { City = "Milan", Code = "MXP", Name = "Milan Malpensa Airport" } }, + { "Frankfurt", new AirportInfo { City = "Frankfurt", Code = "FRA", Name = "Frankfurt am Main Airport" } }, + { "Munich", new AirportInfo { City = "Munich", Code = "MUC", Name = "Munich Airport" } }, + { + "Mumbai", + new AirportInfo + { City = "Mumbai", Code = "BOM", Name = "Chhatrapati Shivaji Maharaj International Airport" } + }, + { "Cape Town", new AirportInfo { City = "Cape Town", Code = "CPT", Name = "Cape Town International Airport" } }, + { "Zurich", new AirportInfo { City = "Zurich", Code = "ZRH", Name = "Zurich Airport" } }, + { "Vienna", new AirportInfo { City = "Vienna", Code = "VIE", Name = "Vienna International Airport" } } + // Add more airports as needed + }; + + public AirportInfo GetAirportInfoForCity(string city) + { + if (_airportsByCity.TryGetValue(city, out var airportInfo)) + { + return airportInfo; + } + + throw new KeyNotFoundException($"No airport information found for city: {city}"); + } +} + +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}"; + } +} \ No newline at end of file diff --git a/examples/Event Handler/BedrockAgentFunction/src/BedrockAgentFunction.csproj b/examples/Event Handler/BedrockAgentFunction/src/BedrockAgentFunction.csproj new file mode 100644 index 00000000..bcd2c51c --- /dev/null +++ b/examples/Event Handler/BedrockAgentFunction/src/BedrockAgentFunction.csproj @@ -0,0 +1,22 @@ + + + Exe + net8.0 + enable + enable + true + Lambda + + true + + true + + + + + + + + + + \ No newline at end of file diff --git a/examples/Event Handler/BedrockAgentFunction/src/Function.cs b/examples/Event Handler/BedrockAgentFunction/src/Function.cs new file mode 100644 index 00000000..51fbf144 --- /dev/null +++ b/examples/Event Handler/BedrockAgentFunction/src/Function.cs @@ -0,0 +1,55 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using AWS.Lambda.Powertools.EventHandler.Resolvers; +using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; +using AWS.Lambda.Powertools.Logging; +using BedrockAgentFunction; +using Microsoft.Extensions.Logging; + + +var logger = LoggerFactory.Create(builder => +{ + builder.AddPowertoolsLogger(config => { config.Service = "AirportService"; }); +}).CreatePowertoolsLogger(); + +var resolver = new BedrockAgentFunctionResolver(); + + +resolver.Tool("getAirportCodeForCity", "Get airport code and full name for a specific city", (string city, ILambdaContext context) => +{ + logger.LogInformation($"Getting airport code for city: {city}"); + var airportService = new AirportService(); + var airportInfo = airportService.GetAirportInfoForCity(city); + + logger.LogInformation($"Airport for {city}: {airportInfo.Code} - {airportInfo.Name}"); + + // Note: Best approach is to override the ToString method in the AirportInfo class + // public override string ToString() + // { + // return $"{Name} ({Code}) in {City}"; + // } + // This will return a string with properties Code and Name + return airportInfo; + + //Alternatively, you can return an anonymous object + // return new { + // airportInfo + // }; +}); + + +// 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(); + + diff --git a/examples/Event Handler/BedrockAgentFunction/src/Readme.md b/examples/Event Handler/BedrockAgentFunction/src/Readme.md new file mode 100644 index 00000000..d0cfb668 --- /dev/null +++ b/examples/Event Handler/BedrockAgentFunction/src/Readme.md @@ -0,0 +1,47 @@ +# Powertools for AWS Lambda .NET - Bedrock Agent Function example + +This starter project consists of: +* Function.cs - file contain C# top level statements that define the function to be called for each event and starts the Lambda runtime client. +* AirportService.cs - Static list of airport codes and names used by the function. +* aws-lambda-tools-defaults.json - default argument settings for use with Visual Studio and command line deployment tools for AWS + +## Executable Assembly + +.NET Lambda projects that use C# top level statements like this project must be deployed as an executable assembly instead of a class library. To indicate to Lambda that the .NET function is an executable assembly the +Lambda function handler value is set to the .NET Assembly name. This is different then deploying as a class library where the function handler string includes the assembly, type and method name. + +To deploy as an executable assembly the Lambda runtime client must be started to listen for incoming events to process. To start +the Lambda runtime client add the `Amazon.Lambda.RuntimeSupport` NuGet package and add the following code at the end of the +of the file containing top-level statements to start the runtime. + +```csharp +await LambdaBootstrapBuilder.Create(handler, new DefaultLambdaJsonSerializer()) + .Build() + .RunAsync(); +``` + +Pass into the Lambda runtime client a function handler as either an `Action<>` or `Func<>` for the code that +should be called for each event. If the handler takes in an input event besides `System.IO.Stream` then +the JSON serializer must also be passed into the `Create` method. + +## Here are some steps to follow to get started from the command line: + +Once you have edited your template and code you can deploy your application using the [Amazon.Lambda.Tools Global Tool](https://github.com/aws/aws-extensions-for-dotnet-cli#aws-lambda-amazonlambdatools) from the command line. + +Install Amazon.Lambda.Tools Global Tools if not already installed. +``` + dotnet tool install -g Amazon.Lambda.Tools +``` + +If already installed check if new version is available. +``` + dotnet tool update -g Amazon.Lambda.Tools +``` + +Deploy function to AWS Lambda +``` + cd "BedrockAgentFunction/src" + dotnet lambda package --output-package ../release/BedrockAgentFunction.zip + cd ../infra + npm run cdk deploy -- --require-approval never +``` \ No newline at end of file diff --git a/examples/Event Handler/BedrockAgentFunction/src/aws-lambda-tools-defaults.json b/examples/Event Handler/BedrockAgentFunction/src/aws-lambda-tools-defaults.json new file mode 100644 index 00000000..1dc447ae --- /dev/null +++ b/examples/Event Handler/BedrockAgentFunction/src/aws-lambda-tools-defaults.json @@ -0,0 +1,15 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "", + "region": "", + "configuration": "Release", + "function-runtime": "dotnet8", + "function-memory-size": 512, + "function-timeout": 30, + "function-handler": "BedrockAgentFunction" +} \ No newline at end of file From 93f08913e26b6db42be672eacba2f5013073f2d5 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:31:50 +0100 Subject: [PATCH 49/52] docs: update Bedrock Agent Function documentation with new features and examples --- .../event_handler/bedrock_agent_function.md | 263 +++++++++++++----- .../infra/lib/bedrockagents-stack.ts | 4 +- 2 files changed, 189 insertions(+), 78 deletions(-) diff --git a/docs/core/event_handler/bedrock_agent_function.md b/docs/core/event_handler/bedrock_agent_function.md index f9acae45..f2cbc2da 100644 --- a/docs/core/event_handler/bedrock_agent_function.md +++ b/docs/core/event_handler/bedrock_agent_function.md @@ -18,28 +18,24 @@ 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 -- **Simple Tool Registration**: Register functions with descriptive names that Bedrock Agents can invoke -- **Automatic Parameter Handling**: Parameters are automatically extracted from Bedrock Agent requests and converted to the appropriate types -- **Lambda Context Access**: Easy access to Lambda context for logging and AWS Lambda features -- **Dependency Injection Support**: Seamless integration with .NET's dependency injection system -- **AOT Compatibility**: Fully compatible with .NET 8 AOT compilation through source generation +* 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 @@ -66,67 +62,202 @@ dotnet add package AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunc 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. -??? note "Click to see example IaC templates" - -TODO: add cdk - +??? note "Click to see example SAM template" + ```yaml + 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. -```csharp -using Amazon.BedrockAgentRuntime.Model; -using Amazon.Lambda.Core; -using AWS.Lambda.Powertools.EventHandler; +=== "Executable asembly" -[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + ```csharp + using Amazon.Lambda.Core; + using Amazon.Lambda.RuntimeSupport; + using AWS.Lambda.Powertools.EventHandler.Resolvers; + using AWS.Lambda.Powertools.EventHandler.Resolvers.BedrockAgentFunction.Models; -namespace MyLambdaFunction -{ - public class Function + 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) => { - private readonly BedrockAgentFunctionResolver _resolver; - - public Function() + 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(); + ``` + +=== "Class Library" + + ```csharp + 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 { - _resolver = new BedrockAgentFunctionResolver(); + private readonly BedrockAgentFunctionResolver _resolver; - // 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 ActionGroupInvocationOutput FunctionHandler( - ActionGroupInvocationInput input, ILambdaContext context) - { - return _resolver.Resolve(input, context); + 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. +## 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: + - Finds the registered handler for the requested function + - Extracts and converts parameters to the correct types + - Invokes your handler with the parameters + - Formats the response in the way Bedrock Agents expect +5. The agent receives the response and uses it to continue the conversation with the user + ## Advanced Usage -### Functions with Descriptions +### Custom type serialization -Add descriptive information to your tool functions: +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. -```csharp -_resolver.Tool( - "CheckInventory", - "Checks if a product is available in inventory", - (string productId, bool checkWarehouse) => +```csharp hl_lines="4" +resolver.Tool( + name: "PriceCalculator", + description: "Calculate total price with tax", + handler: (MyCustomType myCustomType) => { - return checkWarehouse - ? $"Product {productId} has 15 units in warehouse" - : $"Product {productId} has 5 units in store"; - }); + 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. + +```csharp hl_lines="1 5 12-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 @@ -134,7 +265,7 @@ _resolver.Tool( You can access to the original Lambda event or context for additional information. These are passed to the handler function as optional arguments. ```csharp -_resolver.Tool( +resolver.Tool( "LogRequest", "Logs request information and returns confirmation", (string requestId, ILambdaContext context) => @@ -177,6 +308,7 @@ resolver.Tool("CustomFailure", () => }; }); ``` + ### 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. @@ -254,7 +386,7 @@ Access the raw Bedrock Agent request: _resolver.Tool( "ProcessRawRequest", "Processes the raw Bedrock Agent request", - (ActionGroupInvocationInput input) => + (BedrockFunctionRequest input) => { var functionName = input.Function; var parameterCount = input.Parameters.Count; @@ -288,30 +420,6 @@ resolver.Tool( }); ``` -## 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: - - Finds the registered handler for the requested function - - Extracts and converts parameters to the correct types - - Invokes your handler with the parameters - - Formats the response in the way Bedrock Agents expect -5. The agent receives the response and uses it to continue the conversation with the user - -## Supported Parameter Types - -- `string` -- `int` -- `number` -- `bool` -- `enum` types -- `ILambdaContext` (for accessing Lambda context) -- `ActionGroupInvocationInput` (for accessing raw request) -- Any service registered in dependency injection - - ## 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: @@ -361,6 +469,9 @@ var resolver = serviceProvider.GetRequiredService( ## Complete Example with Dependency Injection +You can find examples in the [Powertools for AWS Lambda (.NET) GitHub repository](https://github.com/aws-powertools/powertools-lambda-dotnet/tree/develop/examples/Event%20Handler/BedrockAgentFunction). + + ```csharp using Amazon.BedrockAgentRuntime.Model; using Amazon.Lambda.Core; diff --git a/examples/Event Handler/BedrockAgentFunction/infra/lib/bedrockagents-stack.ts b/examples/Event Handler/BedrockAgentFunction/infra/lib/bedrockagents-stack.ts index bf08fb81..59b80ebc 100644 --- a/examples/Event Handler/BedrockAgentFunction/infra/lib/bedrockagents-stack.ts +++ b/examples/Event Handler/BedrockAgentFunction/infra/lib/bedrockagents-stack.ts @@ -7,7 +7,7 @@ import { Duration, } from 'aws-cdk-lib'; import type { Construct } from 'constructs'; -import { Runtime, Function, Code, Architecture } from 'aws-cdk-lib/aws-lambda'; +import { Runtime, Function as LambdaFunction, Code, Architecture } from 'aws-cdk-lib/aws-lambda'; import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; import { CfnAgent } from 'aws-cdk-lib/aws-bedrock'; import { @@ -29,7 +29,7 @@ export class BedrockAgentsStack extends Stack { retention: RetentionDays.ONE_DAY, }); - const fn = new Function(this, 'MyFunction', { + const fn = new LambdaFunction(this, 'MyFunction', { functionName: fnName, logGroup, timeout: Duration.minutes(3), From f16b932c30440513de260a92e2a8a08dfec9f14d Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:57:56 +0100 Subject: [PATCH 50/52] docs: enhance Bedrock Agent Function documentation with response format details and examples --- .../event_handler/bedrock_agent_function.md | 42 +++++++++++++++++++ .../infra/lib/bedrockagents-stack.ts | 1 - 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/core/event_handler/bedrock_agent_function.md b/docs/core/event_handler/bedrock_agent_function.md index f2cbc2da..e6884368 100644 --- a/docs/core/event_handler/bedrock_agent_function.md +++ b/docs/core/event_handler/bedrock_agent_function.md @@ -208,6 +208,48 @@ To create an agent, use the `BedrockAgentFunctionResolver` to register your tool ``` 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. + +```csharp +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. diff --git a/examples/Event Handler/BedrockAgentFunction/infra/lib/bedrockagents-stack.ts b/examples/Event Handler/BedrockAgentFunction/infra/lib/bedrockagents-stack.ts index 59b80ebc..001d9912 100644 --- a/examples/Event Handler/BedrockAgentFunction/infra/lib/bedrockagents-stack.ts +++ b/examples/Event Handler/BedrockAgentFunction/infra/lib/bedrockagents-stack.ts @@ -11,7 +11,6 @@ import { Runtime, Function as LambdaFunction, Code, Architecture } from 'aws-cdk import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; import { CfnAgent } from 'aws-cdk-lib/aws-bedrock'; import { - Effect, PolicyDocument, PolicyStatement, Role, From 35df0c18dcabe63d776b14418f631f2db10add23 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:02:57 +0100 Subject: [PATCH 51/52] refactor: improve logging format in getAirportCodeForCity function --- .../BedrockAgentFunction/src/Function.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/examples/Event Handler/BedrockAgentFunction/src/Function.cs b/examples/Event Handler/BedrockAgentFunction/src/Function.cs index 51fbf144..c4e847ef 100644 --- a/examples/Event Handler/BedrockAgentFunction/src/Function.cs +++ b/examples/Event Handler/BedrockAgentFunction/src/Function.cs @@ -18,24 +18,14 @@ resolver.Tool("getAirportCodeForCity", "Get airport code and full name for a specific city", (string city, ILambdaContext context) => { - logger.LogInformation($"Getting airport code for city: {city}"); + logger.LogInformation("Getting airport code for city: {City}", city); var airportService = new AirportService(); var airportInfo = airportService.GetAirportInfoForCity(city); - logger.LogInformation($"Airport for {city}: {airportInfo.Code} - {airportInfo.Name}"); + logger.LogInformation("Airport for {City}: {AirportInfoCode} - {AirportInfoName}", city, airportInfo.Code, airportInfo.Name); // Note: Best approach is to override the ToString method in the AirportInfo class - // public override string ToString() - // { - // return $"{Name} ({Code}) in {City}"; - // } - // This will return a string with properties Code and Name return airportInfo; - - //Alternatively, you can return an anonymous object - // return new { - // airportInfo - // }; }); From 1aa7b2780a0a18dcbd8e8cde2037150a508f5d20 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:15:41 +0100 Subject: [PATCH 52/52] Update version.json Signed-off-by: Henrique Graca <999396+hjgraca@users.noreply.github.com> --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 92675f91..fd3b9502 100644 --- a/version.json +++ b/version.json @@ -10,6 +10,6 @@ "Idempotency": "1.3.0", "BatchProcessing": "1.2.1", "EventHandler": "1.0.0", - "EventHandler.Resolvers.BedrockAgentFunction": "1.0.0-alpha.2" + "EventHandler.Resolvers.BedrockAgentFunction": "1.0.0" } }