Skip to content

feature: bedrock agent api support #2019

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Libraries/Libraries.sln
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.DynamoDBEvent
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests", "test\Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests\Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests.csproj", "{074DB940-82BA-47D4-B888-C213D4220A82}"
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
Expand Down Expand Up @@ -381,6 +383,10 @@ Global
{074DB940-82BA-47D4-B888-C213D4220A82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{074DB940-82BA-47D4-B888-C213D4220A82}.Release|Any CPU.ActiveCfg = Release|Any CPU
{074DB940-82BA-47D4-B888-C213D4220A82}.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
Expand Down Expand Up @@ -449,6 +455,7 @@ Global
{A699E183-D0D4-4F26-A0A7-88DA5607F455} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
{3400F4E9-BA12-4D3D-9BA1-2798AA8D0AFC} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12}
{074DB940-82BA-47D4-B888-C213D4220A82} = {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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<ProjectReference Include="..\Amazon.Lambda.APIGatewayEvents\Amazon.Lambda.APIGatewayEvents.csproj" />
<ProjectReference Include="..\Amazon.Lambda.AspNetCoreServer\Amazon.Lambda.AspNetCoreServer.csproj" />
<ProjectReference Include="..\Amazon.Lambda.RuntimeSupport\Amazon.Lambda.RuntimeSupport.csproj" />
<ProjectReference Include="..\Amazon.Lambda.BedrockAgentEvents\Amazon.Lambda.BedrockAgentEvents.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,45 @@ public ApplicationLoadBalancerMinimalApi(IServiceProvider serviceProvider)
}
}
}

/// <summary>
/// IServer for handling Lambda events from Amazon Bedrock Agent API.
/// </summary>
public class BedrockAgentApiLambdaRuntimeSupportServer : LambdaRuntimeSupportServer
{
/// <summary>
/// Create instances
/// </summary>
/// <param name="serviceProvider">The IServiceProvider created for the ASP.NET Core application</param>
public BedrockAgentApiLambdaRuntimeSupportServer(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}

/// <summary>
/// Creates HandlerWrapper for processing events from Bedrock Agent API
/// </summary>
/// <param name="serviceProvider"></param>
/// <returns></returns>
protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceProvider)
{
var handler = new BedrockAgentApiMinimalApi(serviceProvider).FunctionHandlerAsync;
return HandlerWrapper.GetHandlerWrapper(handler, this.Serializer);
}

/// <summary>
/// Create the BedrockAgentApiFunction passing in the ASP.NET Core application's IServiceProvider
/// </summary>
public class BedrockAgentApiMinimalApi : BedrockAgentApiFunction
{
/// <summary>
/// Create instances
/// </summary>
/// <param name="serviceProvider">The IServiceProvider created for the ASP.NET Core application</param>
public BedrockAgentApiMinimalApi(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
}
}
}
12 changes: 12 additions & 0 deletions Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
```
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ public enum LambdaEventSource
/// <summary>
/// ELB Application Load Balancer
/// </summary>
ApplicationLoadBalancer
ApplicationLoadBalancer,

/// <summary>
/// Amazon Bedrock Agent API
/// </summary>
BedrockAgentApi
}

/// <summary>
Expand Down Expand Up @@ -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")
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<ProjectReference Include="..\Amazon.Lambda.Core\Amazon.Lambda.Core.csproj" />
<ProjectReference Include="..\Amazon.Lambda.Logging.AspNetCore\Amazon.Lambda.Logging.AspNetCore.csproj" />
<ProjectReference Include="..\Amazon.Lambda.APIGatewayEvents\Amazon.Lambda.APIGatewayEvents.csproj" />
<ProjectReference Include="..\Amazon.Lambda.BedrockAgentEvents\Amazon.Lambda.BedrockAgentEvents.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
public abstract class BedrockAgentApiFunction : AbstractAspNetCoreFunction<BedrockAgentApiRequest, BedrockAgentApiResponse>
{
/// <summary>
/// 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.
/// </summary>
private readonly JsonSerializerContext _serializerContext;
private readonly JsonSerializerOptions _jsonSerializerOptions = new()
{
WriteIndented = true,
PropertyNameCaseInsensitive = true
};

/// <summary>
/// The constructor for the BedrockAgentApiFunction.
/// </summary>
protected BedrockAgentApiFunction() : base()
{
_serializerContext = new BedrockAgentApiSerializerContext(_jsonSerializerOptions);
}

/// <summary>
/// The constructor for the BedrockAgentApiFunction that takes in the IServiceProvider for the ASP.NET Core application.
/// </summary>
/// <param name="services">The IServiceProvider for the ASP.NET Core application.</param>
protected BedrockAgentApiFunction(IServiceProvider services) : base(services)
{
_serializerContext = new BedrockAgentApiSerializerContext(_jsonSerializerOptions);
}

/// <summary>
/// Constructor that allows configuring when the ASP.NET Core framework will be initialized
/// </summary>
/// <param name="startupMode">Configure when the ASP.NET Core framework will be initialized</param>
protected BedrockAgentApiFunction(StartupMode startupMode) : base(startupMode)
{
_serializerContext = new BedrockAgentApiSerializerContext(_jsonSerializerOptions);
}

/// <summary>
/// Convert the incoming Bedrock Agent API request to ASP.NET Core request features.
/// </summary>
/// <param name="features">The ASP.NET Core request features.</param>
/// <param name="bedrockAgentApiRequest">The Bedrock Agent API request.</param>
/// <param name="lambdaContext">The Lambda context.</param>
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<string>();
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<string, string>();

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<string, string>)) as JsonTypeInfo<Dictionary<string, string>>;
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);
}

/// <summary>
/// Convert the ASP.NET Core response to a Bedrock Agent API response.
/// </summary>
/// <param name="responseFeatures">The ASP.NET Core response features.</param>
/// <param name="lambdaContext">The Lambda context.</param>
/// <param name="statusCodeIfNotSet">The status code to use if not set in the response.</param>
/// <returns>The Bedrock Agent API response.</returns>
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<string, ResponseContent>
{
[contentType] = new ResponseContent
{
Body = responseBody
}
}
},
SessionAttributes = request.SessionAttributes ?? [],
PromptSessionAttributes = request.PromptSessionAttributes ?? []
};

PostMarshallResponseFeature(responseFeatures, response, lambdaContext);

return response;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Hosting;
using System.Diagnostics.CodeAnalysis;

namespace Amazon.Lambda.AspNetCoreServer
{
/// <summary>
/// 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.
/// </summary>
/// <typeparam name ="TStartup">The type containing the startup methods for the application.</typeparam>
public abstract class BedrockAgentApiFunction<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)] TStartup> : BedrockAgentApiFunction where TStartup : class
{
/// <summary>
/// Default Constructor. The ASP.NET Core Framework will be initialized as part of the construction.
/// </summary>
protected BedrockAgentApiFunction()
: base()
{

}


/// <summary>
///
/// </summary>
/// <param name="startupMode">Configure when the ASP.NET Core framework will be initialized</param>
protected BedrockAgentApiFunction(StartupMode startupMode)
: base(startupMode)
{

}

/// <inheritdoc/>
protected override void Init(IWebHostBuilder builder)
{
builder.UseStartup<TStartup>();
}

}
}
Loading