Skip to content

chore(event-handler): align implementation with other runtimes #3989

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

Merged
merged 8 commits into from
May 28, 2025
Merged
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
4 changes: 3 additions & 1 deletion packages/event-handler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ You can use the library in both TypeScript and JavaScript code bases.

## Intro

Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Balancer (ALB), Lambda Function URLs, and VPC Lattice.
Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Balancer (ALB), Lambda Function URLs, VPC Lattice, AWS AppSync Events APIs, and Amazon Bedrock Agent Functions.

## Usage

Expand Down Expand Up @@ -102,6 +102,8 @@ export const handler = async (event, context) =>
app.resolve(event, context);
```

## Bedrock Agent Functions

See the [documentation](https://docs.powertools.aws.dev/lambda/typescript/latest/features/event-handler/appsync-events) for more details on how to use the AppSync event handler.

## Contribute
Expand Down
256 changes: 155 additions & 101 deletions packages/event-handler/src/bedrock-agent/BedrockAgentFunctionResolver.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,60 @@
import { EnvironmentVariablesService } from '@aws-lambda-powertools/commons';
import { isNullOrUndefined } from '@aws-lambda-powertools/commons/typeutils';
import { getStringFromEnv } from '@aws-lambda-powertools/commons/utils/env';
import type { Context } from 'aws-lambda';
import type {
BedrockAgentFunctionResponse,
Configuration,
ParameterValue,
ResolverOptions,
ResponseOptions,
Tool,
ToolFunction,
} from '../types/bedrock-agent.js';
import type { GenericLogger } from '../types/common.js';
import { BedrockFunctionResponse } from './BedrockFunctionResponse.js';
import { assertBedrockAgentFunctionEvent } from './utils.js';

export class BedrockAgentFunctionResolver {
/**
* Resolver for AWS Bedrock Agent Function invocations.
*
* This resolver is designed to handle function invocations from Bedrock Agents.
*
* @example
* ```ts
* import {
* BedrockAgentFunctionResolver
* } from '@aws-lambda-powertools/event-handler/bedrock-agent';
*
* const app = new BedrockAgentFunctionResolver();
*
* app.tool(async (params) => {
* const { name } = params;
* return `Hello, ${name}!`;
* }, {
* name: 'greeting',
* description: 'Greets a person by name',
* });
*
* export const handler = async (event, context) =>
* app.resolve(event, context);
* ```
*/
class BedrockAgentFunctionResolver {
/**
* Registry of tools added to the Bedrock Agent Function Resolver.
*/
readonly #tools: Map<string, Tool> = new Map();
readonly #envService: EnvironmentVariablesService;
/**
* A logger instance to be used for logging debug, warning, and error messages.
*
* When no logger is provided, we'll only log warnings and errors using the global `console` object.
*/
readonly #logger: Pick<GenericLogger, 'debug' | 'warn' | 'error'>;

constructor(options?: ResolverOptions) {
this.#envService = new EnvironmentVariablesService();
const alcLogLevel = this.#envService.get('AWS_LAMBDA_LOG_LEVEL');
const alcLogLevel = getStringFromEnv({
key: 'AWS_LAMBDA_LOG_LEVEL',
defaultValue: '',
});
this.#logger = options?.logger ?? {
debug: alcLogLevel === 'DEBUG' ? console.debug : () => {},
error: console.error,
Expand All @@ -34,7 +69,9 @@ export class BedrockAgentFunctionResolver {
*
* @example
* ```ts
* import { BedrockAgentFunctionResolver } from '@aws-lambda-powertools/event-handler/bedrock-agent-function';
* import {
* BedrockAgentFunctionResolver
* } from '@aws-lambda-powertools/event-handler/bedrock-agent';
*
* const app = new BedrockAgentFunctionResolver();
*
Expand All @@ -50,113 +87,106 @@ export class BedrockAgentFunctionResolver {
* app.resolve(event, context);
* ```
*
* The method also works as a class method decorator:
* If you know the function signature, you can also use a type parameter to specify the parameters of the tool function:
*
* @example
* ```ts
* import {
* BedrockAgentFunctionResolver,
* } from '@aws-lambda-powertools/event-handler/bedrock-agent';
*
* const app = new BedrockAgentFunctionResolver();
*
* app.tool<{ name: string }>(async (params) => {
* const { name } = params;
* // ^ name: string
* return `Hello, ${name}!`;
* }, {
* name: 'greeting',
* description: 'Greets a person by name',
* });
*
* export const handler = async (event, context) =>
* app.resolve(event, context);
* ```
*
* When defining a tool, you can also access the original `event` and `context` objects from the Bedrock Agent function invocation.
* This is useful if you need to access the session attributes or other context-specific information.
*
* @example
* ```ts
* import { BedrockAgentFunctionResolver } from '@aws-lambda-powertools/event-handler/bedrock-agent-function';
* import {
* BedrockAgentFunctionResolver
* } from '@aws-lambda-powertools/event-handler/bedrock-agent';
*
* const app = new BedrockAgentFunctionResolver();
*
* class Lambda {
* @app.tool({ name: 'greeting', description: 'Greets a person by name' })
* async greeting(params) {
* const { name } = params;
* return `Hello, ${name}!`;
* }
* app.tool(async (params, { event, context }) => {
* const { name } = params;
* // Access session attributes from the event
* const sessionAttributes = event.sessionAttributes || {};
* // You can also access the context if needed
* sessionAttributes.requestId = context.awsRequestId;
*
* async handler(event, context) {
* return app.resolve(event, context);
* }
* }
* return `Hello, ${name}!`;
* }, {
* name: 'greetingWithContext',
* description: 'Greets a person by name',
* });
*
* const lambda = new Lambda();
* export const handler = lambda.handler.bind(lambda);
* export const handler = async (event, context) =>
* app.resolve(event, context);
* ```
*
* @param fn - The tool function
* @param config - The configuration object for the tool
* @param config.name - The name of the tool, which must be unique across all registered tools.
* @param config.description - A description of the tool, which is optional but highly recommended.
*/
public tool<TParams extends Record<string, ParameterValue>>(
fn: ToolFunction<TParams>,
config: Configuration
): undefined;
public tool<TParams extends Record<string, ParameterValue>>(
config: Configuration
): MethodDecorator;
public tool<TParams extends Record<string, ParameterValue>>(
fnOrConfig: ToolFunction<TParams> | Configuration,
config?: Configuration
): MethodDecorator | undefined {
// When used as a method (not a decorator)
if (typeof fnOrConfig === 'function') {
this.#registerTool(fnOrConfig, config as Configuration);
return;
}

// When used as a decorator
return (_target, _propertyKey, descriptor: PropertyDescriptor) => {
const toolFn = descriptor.value as ToolFunction;
this.#registerTool(toolFn, fnOrConfig);
return descriptor;
};
}

#registerTool<TParams extends Record<string, ParameterValue>>(
handler: ToolFunction<TParams>,
config: Configuration
): void {
): undefined {
const { name } = config;

if (this.#tools.size >= 5) {
this.#logger.warn(
`The maximum number of tools that can be registered is 5. Tool ${name} will not be registered.`
);
return;
}

if (this.#tools.has(name)) {
this.#logger.warn(
`Tool ${name} already registered. Overwriting with new definition.`
`Tool "${name}" already registered. Overwriting with new definition.`
);
}

this.#tools.set(name, {
handler: handler as ToolFunction,
handler: fn as ToolFunction,
config,
});
this.#logger.debug(`Tool ${name} has been registered.`);
}

#buildResponse(options: ResponseOptions): BedrockAgentFunctionResponse {
const {
actionGroup,
function: func,
body,
errorType,
sessionAttributes,
promptSessionAttributes,
} = options;

return {
messageVersion: '1.0',
response: {
actionGroup,
function: func,
functionResponse: {
responseState: errorType,
responseBody: {
TEXT: {
body,
},
},
},
},
sessionAttributes,
promptSessionAttributes,
};
this.#logger.debug(`Tool "${name}" has been registered.`);
}

/**
* Resolve an incoming Bedrock Agent function invocation event.
*
* @example
* ```ts
* import {
* BedrockAgentFunctionResolver
* } from '@aws-lambda-powertools/event-handler/bedrock-agent';
*
* const app = new BedrockAgentFunctionResolver();
*
* app.tool(async (params) => {
* const { name } = params;
* return `Hello, ${name}!`;
* }, {
* name: 'greeting',
* description: 'Greets a person by name',
* });
*
* export const handler = async (event, context) =>
* app.resolve(event, context);
* ```
*
* @param event - The incoming payload of the AWS Lambda function.
* @param context - The context object provided by AWS Lambda, which contains information about the invocation, function, and execution environment.
*/
async resolve(
event: unknown,
context: Context
Expand All @@ -169,16 +199,21 @@ export class BedrockAgentFunctionResolver {
actionGroup,
sessionAttributes,
promptSessionAttributes,
knowledgeBasesConfiguration,
} = event;

const tool = this.#tools.get(toolName);

if (tool == null) {
this.#logger.error(`Tool ${toolName} has not been registered.`);
return this.#buildResponse({
this.#logger.error(`Tool "${toolName}" has not been registered.`);
return new BedrockFunctionResponse({
body: `Error: tool "${toolName}" has not been registered.`,
sessionAttributes,
promptSessionAttributes,
knowledgeBasesConfiguration,
}).build({
actionGroup,
function: toolName,
body: 'Error: tool has not been registered in handler.',
func: toolName,
});
}

Expand All @@ -195,7 +230,7 @@ export class BedrockAgentFunctionResolver {
break;
}
// this default will also catch array types but we leave them as strings
// because we cannot reliably parse them
// because we cannot reliably parse them - see discussion in #3710
default: {
toolParams[param.name] = param.value;
break;
Expand All @@ -204,24 +239,43 @@ export class BedrockAgentFunctionResolver {
}

try {
const res = await tool.handler(toolParams, { event, context });
const body = res == null ? '' : JSON.stringify(res);
return this.#buildResponse({
actionGroup,
function: toolName,
const response = await tool.handler(toolParams, { event, context });
if (response instanceof BedrockFunctionResponse) {
return response.build({
actionGroup,
func: toolName,
});
}
const body =
isNullOrUndefined(response) || response === ''
? ''
: JSON.stringify(response);
return new BedrockFunctionResponse({
body,
sessionAttributes,
promptSessionAttributes,
knowledgeBasesConfiguration,
}).build({
actionGroup,
func: toolName,
});
} catch (error) {
this.#logger.error(`An error occurred in tool ${toolName}.`, error);
return this.#buildResponse({
actionGroup,
function: toolName,
body: `Error when invoking tool: ${error}`,
const errorMessage =
error instanceof Error
? `${error.name} - ${error.message}`
: String(error);
return new BedrockFunctionResponse({
body: `Unable to complete tool execution due to ${errorMessage}`,
sessionAttributes,
promptSessionAttributes,
knowledgeBasesConfiguration,
}).build({
actionGroup,
func: toolName,
});
}
}
}

export { BedrockAgentFunctionResolver };
Loading
Loading