From 784d19a5f1d5465c2067ce692401f14ed0906fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=C3=A4fsten?= Date: Mon, 10 Mar 2025 23:34:28 +0100 Subject: [PATCH] Bedrock Agent API support --- Libraries/Libraries.sln | 7 + ...zon.Lambda.AspNetCoreServer.Hosting.csproj | 1 + .../Internal/LambdaRuntimeSupportServer.cs | 41 ++++ .../README.md | 12 + .../ServiceCollectionExtensions.cs | 8 +- .../Amazon.Lambda.AspNetCoreServer.csproj | 1 + .../BedrockAgentApiFunction.cs | 212 ++++++++++++++++++ .../BedrockAgentApiFunction{TStartup}.cs | 41 ++++ .../BedrockAgentApiSerializerContext.cs | 18 ++ .../Internal/Utilities.cs | 6 + .../Amazon.Lambda.BedrockAgentEvents.csproj | 33 +++ .../BedrockAgentApiRequest.cs | 156 +++++++++++++ .../BedrockAgentApiResponse.cs | 200 +++++++++++++++++ .../README.md | 52 +++++ ...Amazon.Lambda.AspNetCoreServer.Test.csproj | 6 + .../TestBedrockAgentApiCalls.cs | 126 +++++++++++ .../TestBedrockAgentApiHosting.cs | 105 +++++++++ .../TestBedrockServiceCollectionExtensions.cs | 69 ++++++ .../UtilitiesTest.cs | 24 ++ .../values-get-all-bedrock-agent-request.json | 21 ++ ...s-get-pathparam-bedrock-agent-request.json | 27 +++ ...get-querystring-bedrock-agent-request.json | 32 +++ ...lues-get-single-bedrock-agent-request.json | 21 ++ ...es-put-withbody-bedrock-agent-request.json | 39 ++++ .../TestWebApp/BedrockAgentApiFunction.cs | 8 + 25 files changed, 1265 insertions(+), 1 deletion(-) create mode 100644 Libraries/src/Amazon.Lambda.AspNetCoreServer/BedrockAgentApiFunction.cs create mode 100644 Libraries/src/Amazon.Lambda.AspNetCoreServer/BedrockAgentApiFunction{TStartup}.cs create mode 100644 Libraries/src/Amazon.Lambda.AspNetCoreServer/BedrockAgentApiSerializerContext.cs create mode 100644 Libraries/src/Amazon.Lambda.BedrockAgentEvents/Amazon.Lambda.BedrockAgentEvents.csproj create mode 100644 Libraries/src/Amazon.Lambda.BedrockAgentEvents/BedrockAgentApiRequest.cs create mode 100644 Libraries/src/Amazon.Lambda.BedrockAgentEvents/BedrockAgentApiResponse.cs create mode 100644 Libraries/src/Amazon.Lambda.BedrockAgentEvents/README.md create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestBedrockAgentApiCalls.cs create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestBedrockAgentApiHosting.cs create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestBedrockServiceCollectionExtensions.cs create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-all-bedrock-agent-request.json create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-pathparam-bedrock-agent-request.json create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-querystring-bedrock-agent-request.json create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-single-bedrock-agent-request.json create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-put-withbody-bedrock-agent-request.json create mode 100644 Libraries/test/TestWebApp/BedrockAgentApiFunction.cs diff --git a/Libraries/Libraries.sln b/Libraries/Libraries.sln index 3df1056d9..bc30470f6 100644 --- a/Libraries/Libraries.sln +++ b/Libraries/Libraries.sln @@ -135,6 +135,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SnapshotRestore.Registry", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SnapshotRestore.Registry.Tests", "test\SnapshotRestore.Registry.Tests\SnapshotRestore.Registry.Tests.csproj", "{A699E183-D0D4-4F26-A0A7-88DA5607F455}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.BedrockAgentEvents", "src\Amazon.Lambda.BedrockAgentEvents\Amazon.Lambda.BedrockAgentEvents.csproj", "{5A28C0AA-07D3-B0CF-2169-56278447DFB1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -369,6 +371,10 @@ Global {A699E183-D0D4-4F26-A0A7-88DA5607F455}.Debug|Any CPU.Build.0 = Debug|Any CPU {A699E183-D0D4-4F26-A0A7-88DA5607F455}.Release|Any CPU.ActiveCfg = Release|Any CPU {A699E183-D0D4-4F26-A0A7-88DA5607F455}.Release|Any CPU.Build.0 = Release|Any CPU + {5A28C0AA-07D3-B0CF-2169-56278447DFB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A28C0AA-07D3-B0CF-2169-56278447DFB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A28C0AA-07D3-B0CF-2169-56278447DFB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A28C0AA-07D3-B0CF-2169-56278447DFB1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -435,6 +441,7 @@ Global {7300983D-8FCE-42EA-9B9E-B1C5347D15D8} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} {7261A438-8C1D-47AD-98B0-7678F72E4382} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12} {A699E183-D0D4-4F26-A0A7-88DA5607F455} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} + {5A28C0AA-07D3-B0CF-2169-56278447DFB1} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {503678A4-B8D1-4486-8915-405A3E9CF0EB} diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj index d0c71e18b..78f0a0d6c 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj @@ -27,6 +27,7 @@ + diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs index 05493e244..0a8029e75 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs @@ -175,4 +175,45 @@ public ApplicationLoadBalancerMinimalApi(IServiceProvider serviceProvider) } } } + + /// + /// IServer for handling Lambda events from Amazon Bedrock Agent API. + /// + public class BedrockAgentApiLambdaRuntimeSupportServer : LambdaRuntimeSupportServer + { + /// + /// Create instances + /// + /// The IServiceProvider created for the ASP.NET Core application + public BedrockAgentApiLambdaRuntimeSupportServer(IServiceProvider serviceProvider) + : base(serviceProvider) + { + } + + /// + /// Creates HandlerWrapper for processing events from Bedrock Agent API + /// + /// + /// + protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceProvider) + { + var handler = new BedrockAgentApiMinimalApi(serviceProvider).FunctionHandlerAsync; + return HandlerWrapper.GetHandlerWrapper(handler, this.Serializer); + } + + /// + /// Create the BedrockAgentApiFunction passing in the ASP.NET Core application's IServiceProvider + /// + public class BedrockAgentApiMinimalApi : BedrockAgentApiFunction + { + /// + /// Create instances + /// + /// The IServiceProvider created for the ASP.NET Core application + public BedrockAgentApiMinimalApi(IServiceProvider serviceProvider) + : base(serviceProvider) + { + } + } + } } \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/README.md b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/README.md index bf3455a65..742f5d620 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/README.md +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/README.md @@ -48,4 +48,16 @@ app.MapControllers(); app.Run(); +``` + +## Amazon Bedrock Agent API + +The `BedrockAgentApi` event source allows your ASP.NET Core application to be invoked by Amazon Bedrock Agent API. This is useful for building custom actions for your Bedrock agents. + +When using the `BedrockAgentApi` event source, the Lambda function will receive events in the Bedrock Agent API format and convert them to HTTP requests that your ASP.NET Core application can process. The responses from your application will be converted back to the Bedrock Agent API format. + +Example: + +```csharp +builder.Services.AddAWSLambdaHosting(LambdaEventSource.BedrockAgentApi); ``` \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs index 82fa10376..46efdd772 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs @@ -27,7 +27,12 @@ public enum LambdaEventSource /// /// ELB Application Load Balancer /// - ApplicationLoadBalancer + ApplicationLoadBalancer, + + /// + /// Amazon Bedrock Agent API + /// + BedrockAgentApi } /// @@ -106,6 +111,7 @@ private static bool TryLambdaSetup(IServiceCollection services, LambdaEventSourc LambdaEventSource.HttpApi => typeof(APIGatewayHttpApiV2LambdaRuntimeSupportServer), LambdaEventSource.RestApi => typeof(APIGatewayRestApiLambdaRuntimeSupportServer), LambdaEventSource.ApplicationLoadBalancer => typeof(ApplicationLoadBalancerLambdaRuntimeSupportServer), + LambdaEventSource.BedrockAgentApi => typeof(BedrockAgentApiLambdaRuntimeSupportServer), _ => throw new ArgumentException($"Event source type {eventSource} unknown") }; diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Amazon.Lambda.AspNetCoreServer.csproj b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Amazon.Lambda.AspNetCoreServer.csproj index 2cfdbfb4f..715b0ee06 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Amazon.Lambda.AspNetCoreServer.csproj +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Amazon.Lambda.AspNetCoreServer.csproj @@ -27,6 +27,7 @@ + diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/BedrockAgentApiFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/BedrockAgentApiFunction.cs new file mode 100644 index 000000000..d75097e25 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/BedrockAgentApiFunction.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using Amazon.Lambda.AspNetCoreServer.Internal; +using Amazon.Lambda.BedrockAgentEvents; +using Amazon.Lambda.Core; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; + +namespace Amazon.Lambda.AspNetCoreServer +{ + /// + /// This class extends from AbstractAspNetCoreFunction which contains the core functionality for converting + /// incoming Amazon Bedrock Agent API events into ASP.NET Core request and then convert the response + /// back to a format that Bedrock Agent API expects. + /// + public abstract class BedrockAgentApiFunction : AbstractAspNetCoreFunction + { + /// + /// The serializer context for the Bedrock Agent API. Used to serialize the incoming parameters from the bedrock request into the ASP.NET Core request body. + /// Required for AOT compliation. + /// + private readonly JsonSerializerContext _serializerContext; + private readonly JsonSerializerOptions _jsonSerializerOptions = new() + { + WriteIndented = true, + PropertyNameCaseInsensitive = true + }; + + /// + /// The constructor for the BedrockAgentApiFunction. + /// + protected BedrockAgentApiFunction() : base() + { + _serializerContext = new BedrockAgentApiSerializerContext(_jsonSerializerOptions); + } + + /// + /// The constructor for the BedrockAgentApiFunction that takes in the IServiceProvider for the ASP.NET Core application. + /// + /// The IServiceProvider for the ASP.NET Core application. + protected BedrockAgentApiFunction(IServiceProvider services) : base(services) + { + _serializerContext = new BedrockAgentApiSerializerContext(_jsonSerializerOptions); + } + + /// + /// Constructor that allows configuring when the ASP.NET Core framework will be initialized + /// + /// Configure when the ASP.NET Core framework will be initialized + protected BedrockAgentApiFunction(StartupMode startupMode) : base(startupMode) + { + _serializerContext = new BedrockAgentApiSerializerContext(_jsonSerializerOptions); + } + + /// + /// Convert the incoming Bedrock Agent API request to ASP.NET Core request features. + /// + /// The ASP.NET Core request features. + /// The Bedrock Agent API request. + /// The Lambda context. + protected override void MarshallRequest(InvokeFeatures features, BedrockAgentApiRequest bedrockAgentApiRequest, ILambdaContext lambdaContext) + { + var path = bedrockAgentApiRequest.ApiPath ?? "/"; + if (!path.StartsWith('/')) + { + path = "/" + path; + } + + var requestFeatures = (IHttpRequestFeature)features; + requestFeatures.Path = Utilities.DecodeResourcePath(path); + requestFeatures.PathBase = string.Empty; + requestFeatures.QueryString = string.Empty; + requestFeatures.Method = bedrockAgentApiRequest.HttpMethod ?? "GET"; + requestFeatures.Scheme = "https"; + requestFeatures.Headers = new HeaderDictionary + { + ["X-Bedrock-Agent-Id"] = bedrockAgentApiRequest.Agent?.Id ?? "", + ["X-Bedrock-Agent-Name"] = bedrockAgentApiRequest.Agent?.Name ?? "", + ["X-Bedrock-Agent-Alias"] = bedrockAgentApiRequest.Agent?.Alias ?? "", + ["X-Bedrock-Agent-Version"] = bedrockAgentApiRequest.Agent?.Version ?? "", + ["X-Bedrock-Session-Id"] = bedrockAgentApiRequest.SessionId ?? "", + ["X-Bedrock-Action-Group"] = bedrockAgentApiRequest.ActionGroup ?? "" + }; + + if (bedrockAgentApiRequest.Parameters != null && bedrockAgentApiRequest.Parameters.Count > 0) + { + var pathParams = Utilities.ExtractPathParams(path); + foreach (var param in bedrockAgentApiRequest.Parameters) + { + if (pathParams.Contains(param.Name)) + { + requestFeatures.Path = requestFeatures.Path.Replace($"{{{param.Name}}}", param.Value); + } + } + + var queryParams = new List(); + foreach (var param in bedrockAgentApiRequest.Parameters) + { + if (pathParams.Contains(param.Name)) + { + continue; + } + queryParams.Add($"{Uri.EscapeDataString(param.Name)}={Uri.EscapeDataString(param.Value)}"); + } + requestFeatures.QueryString = "?" + string.Join("&", queryParams); + } + + long contentLength = 0; + // Only one content type is supported + if (bedrockAgentApiRequest.RequestBody?.Content != null && bedrockAgentApiRequest.RequestBody.Content.Count > 0) + { + // Prioritize application/json content type if exists + var content = bedrockAgentApiRequest.RequestBody.Content.ContainsKey("application/json") + ? bedrockAgentApiRequest.RequestBody.Content.First(x => x.Key == "application/json") + : bedrockAgentApiRequest.RequestBody.Content.First(); + var properties = new Dictionary(); + + if (content.Value.Properties != null) + { + foreach (var prop in content.Value.Properties) + { + properties[prop.Name] = prop.Value; + } + } + + requestFeatures.Headers["Content-Type"] = content.Key; + var jsonTypeInfo = _serializerContext.GetTypeInfo(typeof(Dictionary)) as JsonTypeInfo>; + var body = JsonSerializer.Serialize(properties, jsonTypeInfo); + + var stream = Utilities.ConvertLambdaRequestBodyToAspNetCoreBody(body, false); + + requestFeatures.Body = stream; + contentLength = body.Length; + } + else + { + requestFeatures.Body = new MemoryStream(); + } + + requestFeatures.Headers["Content-Length"] = contentLength.ToString(); + + // Call consumers customize method in case they want to change how API Gateway's request + // was marshalled into ASP.NET Core request. + PostMarshallRequestFeature(requestFeatures, bedrockAgentApiRequest, lambdaContext); + } + + /// + /// Convert the ASP.NET Core response to a Bedrock Agent API response. + /// + /// The ASP.NET Core response features. + /// The Lambda context. + /// The status code to use if not set in the response. + /// The Bedrock Agent API response. + protected override BedrockAgentApiResponse MarshallResponse(IHttpResponseFeature responseFeatures, ILambdaContext lambdaContext, int statusCodeIfNotSet = 200) + { + var itemsFeature = responseFeatures as IItemsFeature; + var request = itemsFeature?.Items[LAMBDA_REQUEST_OBJECT] as BedrockAgentApiRequest; + + if (request == null) + { + throw new InvalidOperationException("The request object was not found in the response features."); + } + + var bodyFeature = responseFeatures as IHttpResponseBodyFeature; + string responseBody = string.Empty; + if (bodyFeature?.Stream != null) + { + responseBody = Encoding.UTF8.GetString(((MemoryStream)bodyFeature.Stream).ToArray()); + } + + // Default content type is application/json, unless otherwise specified. + var contentType = "application/json"; + if (responseFeatures.Headers != null && + responseFeatures.Headers.ContainsKey("Content-Type") && + !responseFeatures.Headers.ContentType.ToString().Contains("application/json")) + { + contentType = responseFeatures.Headers.ContentType; + } + + var response = new BedrockAgentApiResponse + { + MessageVersion = "1.0", + Response = new Response + { + ActionGroup = request.ActionGroup, + ApiPath = request.ApiPath, + HttpMethod = request.HttpMethod, + HttpStatusCode = responseFeatures.StatusCode != 0 ? responseFeatures.StatusCode : statusCodeIfNotSet, + ResponseBody = new Dictionary + { + [contentType] = new ResponseContent + { + Body = responseBody + } + } + }, + SessionAttributes = request.SessionAttributes ?? [], + PromptSessionAttributes = request.PromptSessionAttributes ?? [] + }; + + PostMarshallResponseFeature(responseFeatures, response, lambdaContext); + + return response; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/BedrockAgentApiFunction{TStartup}.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/BedrockAgentApiFunction{TStartup}.cs new file mode 100644 index 000000000..2b108a083 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/BedrockAgentApiFunction{TStartup}.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Hosting; +using System.Diagnostics.CodeAnalysis; + +namespace Amazon.Lambda.AspNetCoreServer +{ + /// + /// BedrockAgentApiFunction is the base class that is implemented in a ASP.NET Core Web API. The derived class implements + /// the Init method similar to Main function in the ASP.NET Core and provides typed Startup. The function handler for + /// the Lambda function will point to this base class FunctionHandlerAsync method. + /// + /// The type containing the startup methods for the application. + public abstract class BedrockAgentApiFunction<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)] TStartup> : BedrockAgentApiFunction where TStartup : class + { + /// + /// Default Constructor. The ASP.NET Core Framework will be initialized as part of the construction. + /// + protected BedrockAgentApiFunction() + : base() + { + + } + + + /// + /// + /// + /// Configure when the ASP.NET Core framework will be initialized + protected BedrockAgentApiFunction(StartupMode startupMode) + : base(startupMode) + { + + } + + /// + protected override void Init(IWebHostBuilder builder) + { + builder.UseStartup(); + } + + } +} diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/BedrockAgentApiSerializerContext.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/BedrockAgentApiSerializerContext.cs new file mode 100644 index 000000000..66fabbb67 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/BedrockAgentApiSerializerContext.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Amazon.Lambda.BedrockAgentEvents; + +namespace Amazon.Lambda.AspNetCoreServer; + +/// +/// Custom serializer context for the Amazon Bedrock Agent API. Used to serialize the incoming parameters from the bedrock request into the ASP.NET Core request body. +/// +[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(BedrockAgentApiRequest))] +[JsonSerializable(typeof(BedrockAgentApiResponse))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Dictionary))] +public partial class BedrockAgentApiSerializerContext : JsonSerializerContext +{ +} diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/Utilities.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/Utilities.cs index 43fac1a96..e9ffdd995 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/Utilities.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/Utilities.cs @@ -145,6 +145,12 @@ public static string CreateQueryStringParametersFromHttpApiV2(string queryString return "?" + queryString; } + public static List ExtractPathParams(string path) + { + var matches = System.Text.RegularExpressions.Regex.Matches(path, @"\{([^}]+)\}"); + return matches.Select(m => m.Groups[1].Value).ToList(); + } + internal static string CreateQueryStringParameters(IDictionary singleValues, IDictionary> multiValues, bool urlEncodeValue) { if (multiValues?.Count > 0) diff --git a/Libraries/src/Amazon.Lambda.BedrockAgentEvents/Amazon.Lambda.BedrockAgentEvents.csproj b/Libraries/src/Amazon.Lambda.BedrockAgentEvents/Amazon.Lambda.BedrockAgentEvents.csproj new file mode 100644 index 000000000..62fcf412d --- /dev/null +++ b/Libraries/src/Amazon.Lambda.BedrockAgentEvents/Amazon.Lambda.BedrockAgentEvents.csproj @@ -0,0 +1,33 @@ + + + + + + Amazon Lambda .NET Core support - Amazon Bedrock Agent API package. + netstandard2.0;net6.0;net8.0 + Amazon.Lambda.BedrockAgentEvents + 1.0.0 + Amazon.Lambda.BedrockAgentEvents + Amazon.Lambda.BedrockAgentEvents + AWS;Amazon;Lambda;Bedrock;Agent + README.md + + + + NETSTANDARD_2_0 + + + + + + + + + + + + IL2026,IL2067,IL2075 + true + true + + \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.BedrockAgentEvents/BedrockAgentApiRequest.cs b/Libraries/src/Amazon.Lambda.BedrockAgentEvents/BedrockAgentApiRequest.cs new file mode 100644 index 000000000..045b2344f --- /dev/null +++ b/Libraries/src/Amazon.Lambda.BedrockAgentEvents/BedrockAgentApiRequest.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; + +namespace Amazon.Lambda.BedrockAgentEvents +{ + /// + /// This class represents the input event from Amazon Bedrock Agent API. + /// + public class BedrockAgentApiRequest + { + /// + /// The version of the message format. + /// + public string MessageVersion { get; set; } + + /// + /// Information about the agent. + /// + public AgentInfo Agent { get; set; } + + /// + /// The input text from the user. + /// + public string InputText { get; set; } + + /// + /// The session ID for the conversation. + /// + public string SessionId { get; set; } + + /// + /// The action group being invoked. + /// + public string ActionGroup { get; set; } + + /// + /// The API path being invoked. + /// + public string ApiPath { get; set; } + + /// + /// The HTTP method being used. + /// + public string HttpMethod { get; set; } + + /// + /// The parameters for the request. + /// + public List Parameters { get; set; } + + /// + /// The request body. + /// + public RequestBody RequestBody { get; set; } + + /// + /// Session attributes that persist for the entire session. + /// + public Dictionary SessionAttributes { get; set; } + + /// + /// Prompt session attributes. + /// + public Dictionary PromptSessionAttributes { get; set; } + } + + /// + /// Information about the agent. + /// + public class AgentInfo + { + /// + /// The name of the agent. + /// + public string Name { get; set; } + + /// + /// The ID of the agent. + /// + public string Id { get; set; } + + /// + /// The alias of the agent. + /// + public string Alias { get; set; } + + /// + /// The version of the agent. + /// + public string Version { get; set; } + } + + /// + /// A parameter for the request. + /// + public class Parameter + { + /// + /// The name of the parameter. + /// + public string Name { get; set; } + + /// + /// The type of the parameter. + /// + public string Type { get; set; } + + /// + /// The value of the parameter. + /// + public string Value { get; set; } + } + + /// + /// The request body. + /// + public class RequestBody + { + /// + /// The content of the request body. Only one content type is supported. + /// + public Dictionary Content { get; set; } + } + + /// + /// Properties for a specific content type. + /// + public class ContentTypeProperties + { + /// + /// The properties for this content type. + /// + public List Properties { get; set; } + } + + /// + /// A property in the request body. + /// + public class Property + { + /// + /// The name of the property. + /// + public string Name { get; set; } + + /// + /// The type of the property. + /// + public string Type { get; set; } + + /// + /// The value of the property. + /// + public string Value { get; set; } + } +} diff --git a/Libraries/src/Amazon.Lambda.BedrockAgentEvents/BedrockAgentApiResponse.cs b/Libraries/src/Amazon.Lambda.BedrockAgentEvents/BedrockAgentApiResponse.cs new file mode 100644 index 000000000..5c5443193 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.BedrockAgentEvents/BedrockAgentApiResponse.cs @@ -0,0 +1,200 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Amazon.Lambda.BedrockAgentEvents +{ + /// + /// This class represents the response event for Amazon Bedrock Agent API. + /// + [DataContract] + public class BedrockAgentApiResponse + { + /// + /// The version of the message format. + /// + [DataMember(Name = "messageVersion")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("messageVersion")] +#endif + public string MessageVersion { get; set; } = "1.0"; + + /// + /// The response details. + /// + [DataMember(Name = "response")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("response")] +#endif + public Response Response { get; set; } + + /// + /// Session attributes that persist for the entire session. + /// + [DataMember(Name = "sessionAttributes")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("sessionAttributes")] +#endif + public Dictionary SessionAttributes { get; set; } + + /// + /// Prompt session attributes. + /// + [DataMember(Name = "promptSessionAttributes")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("promptSessionAttributes")] +#endif + public Dictionary PromptSessionAttributes { get; set; } + + /// + /// Knowledge bases configuration. + /// + [DataMember(Name = "knowledgeBaseConfiguration")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("knowledgeBaseConfiguration")] +#endif + public List KnowledgeBasesConfiguration { get; set; } + } + + /// + /// The response details. + /// + [DataContract] + public class Response + { + /// + /// The action group that was invoked. + /// + [DataMember(Name = "actionGroup")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("actionGroup")] +#endif + public string ActionGroup { get; set; } + + /// + /// The API path that was invoked. + /// + [DataMember(Name = "apiPath")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("apiPath")] +#endif + public string ApiPath { get; set; } + + /// + /// The HTTP method that was used. + /// + [DataMember(Name = "httpMethod")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("httpMethod")] +#endif + public string HttpMethod { get; set; } + + /// + /// The HTTP status code of the response. + /// + [DataMember(Name = "httpStatusCode")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("httpStatusCode")] +#endif + public int HttpStatusCode { get; set; } + + /// + /// The response body. + /// + [DataMember(Name = "responseBody")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("responseBody")] +#endif + public Dictionary ResponseBody { get; set; } + } + + /// + /// The content of the response. + /// + [DataContract] + public class ResponseContent + { + /// + /// The body of the response as a JSON-formatted string. + /// + [DataMember(Name = "body")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("body")] +#endif + public string Body { get; set; } + } + + /// + /// Configuration for a knowledge base. + /// + [DataContract] + public class KnowledgeBaseConfiguration + { + /// + /// The ID of the knowledge base. + /// + [DataMember(Name = "knowledgeBaseId")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("knowledgeBaseId")] +#endif + public string KnowledgeBaseId { get; set; } + + /// + /// The retrieval configuration for the knowledge base. + /// + [DataMember(Name = "retrievalConfiguration")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("retrievalConfiguration")] +#endif + public RetrievalConfiguration RetrievalConfiguration { get; set; } + } + + /// + /// The retrieval configuration for a knowledge base. + /// + [DataContract] + public class RetrievalConfiguration + { + /// + /// The vector search configuration. + /// + [DataMember(Name = "vectorSearchConfiguration")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("vectorSearchConfiguration")] +#endif + public VectorSearchConfiguration VectorSearchConfiguration { get; set; } + } + + /// + /// The vector search configuration. + /// + [DataContract] + public class VectorSearchConfiguration + { + /// + /// The number of results to return. + /// + [DataMember(Name = "numberOfResults")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("numberOfResults")] +#endif + public int NumberOfResults { get; set; } + + /// + /// The search type to use (HYBRID or SEMANTIC). + /// + [DataMember(Name = "overrideSearchType")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("overrideSearchType")] +#endif + public string OverrideSearchType { get; set; } + + /// + /// The filter to apply to the search. + /// + [DataMember(Name = "filter")] +#if NETCOREAPP3_1_OR_GREATER + [System.Text.Json.Serialization.JsonPropertyName("filter")] +#endif + public object Filter { get; set; } + } +} diff --git a/Libraries/src/Amazon.Lambda.BedrockAgentEvents/README.md b/Libraries/src/Amazon.Lambda.BedrockAgentEvents/README.md new file mode 100644 index 000000000..26c074572 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.BedrockAgentEvents/README.md @@ -0,0 +1,52 @@ +# Amazon.Lambda.BedrockAgentEvents + +This package contains classes that can be used as input and output types for AWS Lambda functions that process Amazon Bedrock Agent API events. + +Amazon Bedrock Agent events consist of requests that are routed to a Lambda function by Amazon Bedrock Agents. When this happens, Bedrock expects the result of the function to be the response that should be returned to the agent. + +# Classes + +|Class|Description| +|-----|-----------| +| [BedrockAgentApiRequest](./BedrockAgentApiRequest.cs) | Represents a request coming from an Amazon Bedrock Agent. | +| [BedrockAgentApiResponse](./BedrockAgentApiResponse.cs) | The return object for functions handling requests from Amazon Bedrock Agents. | + +# Sample Function + +The following is a sample class and Lambda function that receives an Amazon Bedrock Agent API event as an input, processes the request, and returns a response. (Note that by default anything written to Console will be logged as CloudWatch Logs events.) + +### Function handler + +```csharp +public class Function +{ + public BedrockAgentApiResponse FunctionHandler(BedrockAgentApiRequest request, ILambdaContext context) + { + Console.WriteLine($"Processing request for action group: {request.ActionGroup}"); + Console.WriteLine($"API Path: {request.ApiPath}, HTTP Method: {request.HttpMethod}"); + + // Process the request + var response = new BedrockAgentApiResponse + { + MessageVersion = "1.0", + Response = new Response + { + ActionGroup = request.ActionGroup, + ApiPath = request.ApiPath, + HttpMethod = request.HttpMethod, + HttpStatusCode = 200, + ResponseBody = new Dictionary + { + ["application/json"] = new ResponseContent + { + Body = "{\"message\":\"Hello from Lambda!\"}" + } + } + }, + SessionAttributes = request.SessionAttributes ?? new Dictionary(), + PromptSessionAttributes = request.PromptSessionAttributes ?? new Dictionary() + }; + + return response; + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj index 0ec5b957c..a9bd468ae 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj @@ -35,12 +35,18 @@ + + + + + + diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestBedrockAgentApiCalls.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestBedrockAgentApiCalls.cs new file mode 100644 index 000000000..28de683f6 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestBedrockAgentApiCalls.cs @@ -0,0 +1,126 @@ +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Amazon.Lambda.BedrockAgentEvents; +using Amazon.Lambda.TestUtilities; +using TestWebApp; +using Xunit; + +namespace Amazon.Lambda.AspNetCoreServer.Test +{ + public class TestBedrockAgentApiCalls + { + [Fact] + public async Task TestGetAllValues() + { + var context = new TestLambdaContext(); + + var response = await InvokeBedrockAgentApiRequest(context, "values-get-all-bedrock-agent-request.json"); + + Assert.Equal(200, response.Response.HttpStatusCode); + Assert.Equal("[\"value1\",\"value2\"]", response.Response.ResponseBody["application/json"].Body); + Assert.Equal("ValuesApi", response.Response.ActionGroup); + Assert.Equal("/api/values", response.Response.ApiPath); + Assert.Equal("GET", response.Response.HttpMethod); + Assert.Equal("1.0", response.MessageVersion); + Assert.Contains("key1", response.SessionAttributes.Keys); + Assert.Equal("value1", response.SessionAttributes["key1"]); + Assert.Contains("key2", response.PromptSessionAttributes.Keys); + Assert.Equal("value2", response.PromptSessionAttributes["key2"]); + + Assert.Contains("OnStarting Called", ((TestLambdaLogger)context.Logger).Buffer.ToString()); + } + + [Fact] + public async Task TestGetSingleValue() + { + var response = await InvokeBedrockAgentApiRequest("values-get-single-bedrock-agent-request.json"); + + Assert.Equal(200, response.Response.HttpStatusCode); + Assert.Equal("value", response.Response.ResponseBody["text/plain; charset=utf-8"].Body); + Assert.Equal("ValuesApi", response.Response.ActionGroup); + Assert.Equal("/api/values/5", response.Response.ApiPath); + Assert.Equal("GET", response.Response.HttpMethod); + } + + [Fact] + public async Task TextGetPathParamValue() + { + var response = await InvokeBedrockAgentApiRequest("values-get-pathparam-bedrock-agent-request.json"); + + Assert.Equal(200, response.Response.HttpStatusCode); + Assert.Equal("value=testid", response.Response.ResponseBody["text/plain; charset=utf-8"].Body); + Assert.Equal("PathParamApi", response.Response.ActionGroup); + Assert.Equal("/api/resourcepath/{id}", response.Response.ApiPath); + Assert.Equal("GET", response.Response.HttpMethod); + } + + [Fact] + public async Task TestGetQueryStringValue() + { + var response = await InvokeBedrockAgentApiRequest("values-get-querystring-bedrock-agent-request.json"); + + Assert.Equal(200, response.Response.HttpStatusCode); + Assert.Equal("Lewis, Meriwether", response.Response.ResponseBody["text/plain; charset=utf-8"].Body); + Assert.Equal("ValuesApi", response.Response.ActionGroup); + Assert.Equal("/api/querystring", response.Response.ApiPath); + Assert.Equal("GET", response.Response.HttpMethod); + } + + [Fact] + public async Task TestPutWithBody() + { + var response = await InvokeBedrockAgentApiRequest("values-put-withbody-bedrock-agent-request.json"); + + Assert.Equal(200, response.Response.HttpStatusCode); + Assert.Equal("Agent, Smith", response.Response.ResponseBody["text/plain; charset=utf-8"].Body); + Assert.Equal("ValuesApi", response.Response.ActionGroup); + Assert.Equal("/api/bodytests", response.Response.ApiPath); + Assert.Equal("PUT", response.Response.HttpMethod); + } + + private async Task InvokeBedrockAgentApiRequest(string fileName) + { + return await InvokeBedrockAgentApiRequest(new TestLambdaContext(), fileName); + } + + private async Task InvokeBedrockAgentApiRequest(TestLambdaContext context, string fileName) + { + var requestContent = GetRequestContent(fileName); + return await InvokeBedrockAgentApiRequestWithContent(context, requestContent); + } + + private async Task InvokeBedrockAgentApiRequestWithContent(TestLambdaContext context, string requestContent) + { + var function = new TestBedrockAgentApiFunction(); + if (function.IncludeUnhandledExceptionDetailInResponse == false) + function.IncludeUnhandledExceptionDetailInResponse = true; + var requestStream = new MemoryStream(Encoding.UTF8.GetBytes(requestContent)); + var request = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer().Deserialize(requestStream); + + return await function.FunctionHandlerAsync(request, context); + } + + private string GetRequestContent(string fileName) + { + var filePath = Path.Combine(Directory.GetCurrentDirectory(), fileName); + if (!File.Exists(filePath)) + { + filePath = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), fileName); + } + return File.ReadAllText(filePath); + } + } + + /// + /// Test implementation of the BedrockAgentApiFunction + /// + public class TestBedrockAgentApiFunction : BedrockAgentApiFunction + { + public TestBedrockAgentApiFunction() + : base(StartupMode.Constructor) + { + IncludeUnhandledExceptionDetailInResponse = true; + } + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestBedrockAgentApiHosting.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestBedrockAgentApiHosting.cs new file mode 100644 index 000000000..9fb852542 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestBedrockAgentApiHosting.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.Lambda.BedrockAgentEvents; +using Amazon.Lambda.Core; +using Amazon.Lambda.Serialization.SystemTextJson; +using Amazon.Lambda.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; +using Amazon.Lambda.AspNetCoreServer.Hosting.Internal; + +namespace Amazon.Lambda.AspNetCoreServer.Test +{ + public class TestBedrockAgentApiHosting + { + [Fact] + public void TestBedrockAgentApiLambdaRuntimeSupportServerCreation() + { + var services = new ServiceCollection(); + services.AddLogging(builder => builder.AddLambdaLogger()); + services.AddSingleton(new DefaultLambdaJsonSerializer()); + var serviceProvider = services.BuildServiceProvider(); + + var server = new BedrockAgentApiLambdaRuntimeSupportServer(serviceProvider); + + Assert.NotNull(server); + } + + [Fact] + public void TestBedrockAgentApiHandlerWrapperCreation() + { + var services = new ServiceCollection(); + services.AddLogging(builder => builder.AddConsole()); + services.AddSingleton(new DefaultLambdaJsonSerializer()); + var serviceProvider = services.BuildServiceProvider(); + var server = new BedrockAgentApiLambdaRuntimeSupportServer(serviceProvider); + + var methodInfo = typeof(BedrockAgentApiLambdaRuntimeSupportServer) + .GetMethod("CreateHandlerWrapper", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + var handlerWrapper = methodInfo.Invoke(server, new object[] { serviceProvider }); + + Assert.NotNull(handlerWrapper); + } + + [Fact] + public void TestBedrockAgentApiMinimalApiCreation() + { + var services = new ServiceCollection(); + services.AddLogging(builder => builder.AddLambdaLogger()); + services.AddSingleton(new DefaultLambdaJsonSerializer()); + var serviceProvider = services.BuildServiceProvider(); + + var minimalApi = new BedrockAgentApiLambdaRuntimeSupportServer.BedrockAgentApiMinimalApi(serviceProvider); + + Assert.NotNull(minimalApi); + } + + [Fact] + public async Task TestBedrockAgentApiMinimalApiHandlerMethod() + { + var services = new ServiceCollection(); + services.AddLogging(builder => builder.AddLambdaLogger()); + services.AddSingleton(new DefaultLambdaJsonSerializer()); + var serviceProvider = services.BuildServiceProvider(); + var minimalApi = new BedrockAgentApiLambdaRuntimeSupportServer.BedrockAgentApiMinimalApi(serviceProvider); + var context = new TestLambdaContext(); + + var request = new BedrockAgentApiRequest + { + MessageVersion = "1.0", + Agent = new AgentInfo + { + Name = "TestAgent", + Id = "agent-12345", + Alias = "TestAlias", + Version = "1.0" + }, + InputText = "Test", + SessionId = "session-12345", + ActionGroup = "TestApi", + ApiPath = "/api/test", + HttpMethod = "GET", + Parameters = new List(), + SessionAttributes = new Dictionary + { + ["key1"] = "value1" + }, + PromptSessionAttributes = new Dictionary + { + ["key2"] = "value2" + } + }; + + var exception = await Assert.ThrowsAsync(async () => + { + await minimalApi.FunctionHandlerAsync(request, context); + }); + + Assert.Contains("application", exception.Message.ToLower()); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestBedrockServiceCollectionExtensions.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestBedrockServiceCollectionExtensions.cs new file mode 100644 index 000000000..c74f3da66 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestBedrockServiceCollectionExtensions.cs @@ -0,0 +1,69 @@ +using System; +using System.Linq; +using Amazon.Lambda.AspNetCoreServer.Hosting.Internal; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Amazon.Lambda.AspNetCoreServer.Test +{ + public class TestBedrockServiceCollectionExtensions + { + [Fact] + public void TestTryLambdaSetupWithBedrockAgentApi() + { + var services = new ServiceCollection(); + Environment.SetEnvironmentVariable("AWS_LAMBDA_FUNCTION_NAME", "TestFunction"); + + try + { + services.AddAWSLambdaHosting(LambdaEventSource.BedrockAgentApi); + var descriptor = services.FirstOrDefault(d => + d.ServiceType == typeof(Microsoft.AspNetCore.Hosting.Server.IServer) && + d.ImplementationType == typeof(BedrockAgentApiLambdaRuntimeSupportServer)); + + Assert.NotNull(descriptor); + } + finally + { + Environment.SetEnvironmentVariable("AWS_LAMBDA_FUNCTION_NAME", null); + } + } + + [Fact] + public void TestAddAWSLambdaHostingWithBedrockAgentApi() + { + var services = new ServiceCollection(); + Environment.SetEnvironmentVariable("AWS_LAMBDA_FUNCTION_NAME", "TestFunction"); + + try + { + services.AddAWSLambdaHosting(LambdaEventSource.BedrockAgentApi); + + var descriptor = services.FirstOrDefault(d => + d.ServiceType == typeof(Microsoft.AspNetCore.Hosting.Server.IServer) && + d.ImplementationType == typeof(BedrockAgentApiLambdaRuntimeSupportServer)); + + Assert.NotNull(descriptor); + } + finally + { + Environment.SetEnvironmentVariable("AWS_LAMBDA_FUNCTION_NAME", null); + } + } + + [Fact] + public void TestAddAWSLambdaHostingWithoutLambdaEnvironment() + { + var services = new ServiceCollection(); + Environment.SetEnvironmentVariable("AWS_LAMBDA_FUNCTION_NAME", null); + + services.AddAWSLambdaHosting(LambdaEventSource.BedrockAgentApi); + + var descriptor = services.FirstOrDefault(d => + d.ServiceType == typeof(Microsoft.AspNetCore.Hosting.Server.IServer) && + d.ImplementationType == typeof(BedrockAgentApiLambdaRuntimeSupportServer)); + + Assert.Null(descriptor); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/UtilitiesTest.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/UtilitiesTest.cs index 8b9463d7e..8df3e5466 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/UtilitiesTest.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/UtilitiesTest.cs @@ -29,5 +29,29 @@ public void EnsureStatusCodeStartsAtIs200() var feature = new InvokeFeatures() as IHttpResponseFeature; Assert.Equal(200, feature.StatusCode); } + + [Fact] + public void TestExtractPathParamsNoParams() + { + var pathParams = Utilities.ExtractPathParams("/api/values"); + Assert.Empty(pathParams); + } + + [Fact] + public void TestExtractPathParamsOneParam() + { + var pathParams = Utilities.ExtractPathParams("/api/values/{id}"); + Assert.Single(pathParams); + Assert.Equal("id", pathParams[0]); + } + + [Fact] + public void TestExtractPathParamsMultipleParams() + { + var pathParams = Utilities.ExtractPathParams("/api/category/{categoryId}/values/{id}"); + Assert.Equal(2, pathParams.Count); + Assert.Equal("categoryId", pathParams[0]); + Assert.Equal("id", pathParams[1]); + } } } diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-all-bedrock-agent-request.json b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-all-bedrock-agent-request.json new file mode 100644 index 000000000..b052fe517 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-all-bedrock-agent-request.json @@ -0,0 +1,21 @@ +{ + "messageVersion": "1.0", + "agent": { + "name": "TestAgent", + "id": "agent-12345", + "alias": "TestAlias", + "version": "1.0" + }, + "inputText": "Get all values", + "sessionId": "session-12345", + "actionGroup": "ValuesApi", + "apiPath": "/api/values", + "httpMethod": "GET", + "parameters": [], + "sessionAttributes": { + "key1": "value1" + }, + "promptSessionAttributes": { + "key2": "value2" + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-pathparam-bedrock-agent-request.json b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-pathparam-bedrock-agent-request.json new file mode 100644 index 000000000..df1f156bd --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-pathparam-bedrock-agent-request.json @@ -0,0 +1,27 @@ +{ + "messageVersion": "1.0", + "agent": { + "name": "TestAgent", + "id": "agent-12345", + "alias": "TestAlias", + "version": "1.0" + }, + "inputText": "Get value with path param", + "sessionId": "session-12345", + "actionGroup": "PathParamApi", + "apiPath": "/api/resourcepath/{id}", + "httpMethod": "GET", + "parameters": [ + { + "name": "id", + "type": "string", + "value": "testid" + } + ], + "sessionAttributes": { + "key1": "value1" + }, + "promptSessionAttributes": { + "key2": "value2" + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-querystring-bedrock-agent-request.json b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-querystring-bedrock-agent-request.json new file mode 100644 index 000000000..553a6e53b --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-querystring-bedrock-agent-request.json @@ -0,0 +1,32 @@ +{ + "messageVersion": "1.0", + "agent": { + "name": "TestAgent", + "id": "agent-12345", + "alias": "TestAlias", + "version": "1.0" + }, + "inputText": "Get values with query parameters", + "sessionId": "session-12345", + "actionGroup": "ValuesApi", + "apiPath": "/api/querystring", + "httpMethod": "GET", + "parameters": [ + { + "name": "firstName", + "type": "string", + "value": "Lewis" + }, + { + "name": "lastName", + "type": "string", + "value": "Meriwether" + } + ], + "sessionAttributes": { + "key1": "value1" + }, + "promptSessionAttributes": { + "key2": "value2" + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-single-bedrock-agent-request.json b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-single-bedrock-agent-request.json new file mode 100644 index 000000000..129988c19 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-get-single-bedrock-agent-request.json @@ -0,0 +1,21 @@ +{ + "messageVersion": "1.0", + "agent": { + "name": "TestAgent", + "id": "agent-12345", + "alias": "TestAlias", + "version": "1.0" + }, + "inputText": "Get value 5", + "sessionId": "session-12345", + "actionGroup": "ValuesApi", + "apiPath": "/api/values/5", + "httpMethod": "GET", + "parameters": [], + "sessionAttributes": { + "key1": "value1" + }, + "promptSessionAttributes": { + "key2": "value2" + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-put-withbody-bedrock-agent-request.json b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-put-withbody-bedrock-agent-request.json new file mode 100644 index 000000000..7ec8413ca --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-put-withbody-bedrock-agent-request.json @@ -0,0 +1,39 @@ +{ + "messageVersion": "1.0", + "agent": { + "name": "TestAgent", + "id": "agent-12345", + "alias": "TestAlias", + "version": "1.0" + }, + "inputText": "Update values", + "sessionId": "session-12345", + "actionGroup": "ValuesApi", + "apiPath": "/api/bodytests", + "httpMethod": "PUT", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "properties": [ + { + "name": "firstName", + "type": "string", + "value": "Smith" + }, + { + "name": "lastName", + "type": "string", + "value": "Agent" + } + ] + } + } + }, + "sessionAttributes": { + "key1": "value1" + }, + "promptSessionAttributes": { + "key2": "value2" + } +} diff --git a/Libraries/test/TestWebApp/BedrockAgentApiFunction.cs b/Libraries/test/TestWebApp/BedrockAgentApiFunction.cs new file mode 100644 index 000000000..1bde92c28 --- /dev/null +++ b/Libraries/test/TestWebApp/BedrockAgentApiFunction.cs @@ -0,0 +1,8 @@ +using Amazon.Lambda.AspNetCoreServer; + +namespace TestWebApp +{ + public class BedrockAgentApiFunction : BedrockAgentApiFunction + { + } +} \ No newline at end of file