Skip to content

Commit 81733be

Browse files
authored
chore(event-handler): align implementation with other runtimes (#3989)
1 parent fce5140 commit 81733be

File tree

8 files changed

+454
-272
lines changed

8 files changed

+454
-272
lines changed

packages/event-handler/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ You can use the library in both TypeScript and JavaScript code bases.
66

77
## Intro
88

9-
Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Balancer (ALB), Lambda Function URLs, and VPC Lattice.
9+
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.
1010

1111
## Usage
1212

@@ -102,6 +102,8 @@ export const handler = async (event, context) =>
102102
app.resolve(event, context);
103103
```
104104

105+
## Bedrock Agent Functions
106+
105107
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.
106108

107109
## Contribute
Lines changed: 155 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,60 @@
1-
import { EnvironmentVariablesService } from '@aws-lambda-powertools/commons';
1+
import { isNullOrUndefined } from '@aws-lambda-powertools/commons/typeutils';
2+
import { getStringFromEnv } from '@aws-lambda-powertools/commons/utils/env';
23
import type { Context } from 'aws-lambda';
34
import type {
45
BedrockAgentFunctionResponse,
56
Configuration,
67
ParameterValue,
78
ResolverOptions,
8-
ResponseOptions,
99
Tool,
1010
ToolFunction,
1111
} from '../types/bedrock-agent.js';
1212
import type { GenericLogger } from '../types/common.js';
13+
import { BedrockFunctionResponse } from './BedrockFunctionResponse.js';
1314
import { assertBedrockAgentFunctionEvent } from './utils.js';
1415

15-
export class BedrockAgentFunctionResolver {
16+
/**
17+
* Resolver for AWS Bedrock Agent Function invocations.
18+
*
19+
* This resolver is designed to handle function invocations from Bedrock Agents.
20+
*
21+
* @example
22+
* ```ts
23+
* import {
24+
* BedrockAgentFunctionResolver
25+
* } from '@aws-lambda-powertools/event-handler/bedrock-agent';
26+
*
27+
* const app = new BedrockAgentFunctionResolver();
28+
*
29+
* app.tool(async (params) => {
30+
* const { name } = params;
31+
* return `Hello, ${name}!`;
32+
* }, {
33+
* name: 'greeting',
34+
* description: 'Greets a person by name',
35+
* });
36+
*
37+
* export const handler = async (event, context) =>
38+
* app.resolve(event, context);
39+
* ```
40+
*/
41+
class BedrockAgentFunctionResolver {
42+
/**
43+
* Registry of tools added to the Bedrock Agent Function Resolver.
44+
*/
1645
readonly #tools: Map<string, Tool> = new Map();
17-
readonly #envService: EnvironmentVariablesService;
46+
/**
47+
* A logger instance to be used for logging debug, warning, and error messages.
48+
*
49+
* When no logger is provided, we'll only log warnings and errors using the global `console` object.
50+
*/
1851
readonly #logger: Pick<GenericLogger, 'debug' | 'warn' | 'error'>;
1952

2053
constructor(options?: ResolverOptions) {
21-
this.#envService = new EnvironmentVariablesService();
22-
const alcLogLevel = this.#envService.get('AWS_LAMBDA_LOG_LEVEL');
54+
const alcLogLevel = getStringFromEnv({
55+
key: 'AWS_LAMBDA_LOG_LEVEL',
56+
defaultValue: '',
57+
});
2358
this.#logger = options?.logger ?? {
2459
debug: alcLogLevel === 'DEBUG' ? console.debug : () => {},
2560
error: console.error,
@@ -34,7 +69,9 @@ export class BedrockAgentFunctionResolver {
3469
*
3570
* @example
3671
* ```ts
37-
* import { BedrockAgentFunctionResolver } from '@aws-lambda-powertools/event-handler/bedrock-agent-function';
72+
* import {
73+
* BedrockAgentFunctionResolver
74+
* } from '@aws-lambda-powertools/event-handler/bedrock-agent';
3875
*
3976
* const app = new BedrockAgentFunctionResolver();
4077
*
@@ -50,113 +87,106 @@ export class BedrockAgentFunctionResolver {
5087
* app.resolve(event, context);
5188
* ```
5289
*
53-
* The method also works as a class method decorator:
90+
* If you know the function signature, you can also use a type parameter to specify the parameters of the tool function:
91+
*
92+
* @example
93+
* ```ts
94+
* import {
95+
* BedrockAgentFunctionResolver,
96+
* } from '@aws-lambda-powertools/event-handler/bedrock-agent';
97+
*
98+
* const app = new BedrockAgentFunctionResolver();
99+
*
100+
* app.tool<{ name: string }>(async (params) => {
101+
* const { name } = params;
102+
* // ^ name: string
103+
* return `Hello, ${name}!`;
104+
* }, {
105+
* name: 'greeting',
106+
* description: 'Greets a person by name',
107+
* });
108+
*
109+
* export const handler = async (event, context) =>
110+
* app.resolve(event, context);
111+
* ```
112+
*
113+
* When defining a tool, you can also access the original `event` and `context` objects from the Bedrock Agent function invocation.
114+
* This is useful if you need to access the session attributes or other context-specific information.
54115
*
55116
* @example
56117
* ```ts
57-
* import { BedrockAgentFunctionResolver } from '@aws-lambda-powertools/event-handler/bedrock-agent-function';
118+
* import {
119+
* BedrockAgentFunctionResolver
120+
* } from '@aws-lambda-powertools/event-handler/bedrock-agent';
58121
*
59122
* const app = new BedrockAgentFunctionResolver();
60123
*
61-
* class Lambda {
62-
* @app.tool({ name: 'greeting', description: 'Greets a person by name' })
63-
* async greeting(params) {
64-
* const { name } = params;
65-
* return `Hello, ${name}!`;
66-
* }
124+
* app.tool(async (params, { event, context }) => {
125+
* const { name } = params;
126+
* // Access session attributes from the event
127+
* const sessionAttributes = event.sessionAttributes || {};
128+
* // You can also access the context if needed
129+
* sessionAttributes.requestId = context.awsRequestId;
67130
*
68-
* async handler(event, context) {
69-
* return app.resolve(event, context);
70-
* }
71-
* }
131+
* return `Hello, ${name}!`;
132+
* }, {
133+
* name: 'greetingWithContext',
134+
* description: 'Greets a person by name',
135+
* });
72136
*
73-
* const lambda = new Lambda();
74-
* export const handler = lambda.handler.bind(lambda);
137+
* export const handler = async (event, context) =>
138+
* app.resolve(event, context);
75139
* ```
76140
*
77141
* @param fn - The tool function
78142
* @param config - The configuration object for the tool
143+
* @param config.name - The name of the tool, which must be unique across all registered tools.
144+
* @param config.description - A description of the tool, which is optional but highly recommended.
79145
*/
80146
public tool<TParams extends Record<string, ParameterValue>>(
81147
fn: ToolFunction<TParams>,
82148
config: Configuration
83-
): undefined;
84-
public tool<TParams extends Record<string, ParameterValue>>(
85-
config: Configuration
86-
): MethodDecorator;
87-
public tool<TParams extends Record<string, ParameterValue>>(
88-
fnOrConfig: ToolFunction<TParams> | Configuration,
89-
config?: Configuration
90-
): MethodDecorator | undefined {
91-
// When used as a method (not a decorator)
92-
if (typeof fnOrConfig === 'function') {
93-
this.#registerTool(fnOrConfig, config as Configuration);
94-
return;
95-
}
96-
97-
// When used as a decorator
98-
return (_target, _propertyKey, descriptor: PropertyDescriptor) => {
99-
const toolFn = descriptor.value as ToolFunction;
100-
this.#registerTool(toolFn, fnOrConfig);
101-
return descriptor;
102-
};
103-
}
104-
105-
#registerTool<TParams extends Record<string, ParameterValue>>(
106-
handler: ToolFunction<TParams>,
107-
config: Configuration
108-
): void {
149+
): undefined {
109150
const { name } = config;
110-
111-
if (this.#tools.size >= 5) {
112-
this.#logger.warn(
113-
`The maximum number of tools that can be registered is 5. Tool ${name} will not be registered.`
114-
);
115-
return;
116-
}
117-
118151
if (this.#tools.has(name)) {
119152
this.#logger.warn(
120-
`Tool ${name} already registered. Overwriting with new definition.`
153+
`Tool "${name}" already registered. Overwriting with new definition.`
121154
);
122155
}
123156

124157
this.#tools.set(name, {
125-
handler: handler as ToolFunction,
158+
handler: fn as ToolFunction,
126159
config,
127160
});
128-
this.#logger.debug(`Tool ${name} has been registered.`);
129-
}
130-
131-
#buildResponse(options: ResponseOptions): BedrockAgentFunctionResponse {
132-
const {
133-
actionGroup,
134-
function: func,
135-
body,
136-
errorType,
137-
sessionAttributes,
138-
promptSessionAttributes,
139-
} = options;
140-
141-
return {
142-
messageVersion: '1.0',
143-
response: {
144-
actionGroup,
145-
function: func,
146-
functionResponse: {
147-
responseState: errorType,
148-
responseBody: {
149-
TEXT: {
150-
body,
151-
},
152-
},
153-
},
154-
},
155-
sessionAttributes,
156-
promptSessionAttributes,
157-
};
161+
this.#logger.debug(`Tool "${name}" has been registered.`);
158162
}
159163

164+
/**
165+
* Resolve an incoming Bedrock Agent function invocation event.
166+
*
167+
* @example
168+
* ```ts
169+
* import {
170+
* BedrockAgentFunctionResolver
171+
* } from '@aws-lambda-powertools/event-handler/bedrock-agent';
172+
*
173+
* const app = new BedrockAgentFunctionResolver();
174+
*
175+
* app.tool(async (params) => {
176+
* const { name } = params;
177+
* return `Hello, ${name}!`;
178+
* }, {
179+
* name: 'greeting',
180+
* description: 'Greets a person by name',
181+
* });
182+
*
183+
* export const handler = async (event, context) =>
184+
* app.resolve(event, context);
185+
* ```
186+
*
187+
* @param event - The incoming payload of the AWS Lambda function.
188+
* @param context - The context object provided by AWS Lambda, which contains information about the invocation, function, and execution environment.
189+
*/
160190
async resolve(
161191
event: unknown,
162192
context: Context
@@ -169,16 +199,21 @@ export class BedrockAgentFunctionResolver {
169199
actionGroup,
170200
sessionAttributes,
171201
promptSessionAttributes,
202+
knowledgeBasesConfiguration,
172203
} = event;
173204

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

176207
if (tool == null) {
177-
this.#logger.error(`Tool ${toolName} has not been registered.`);
178-
return this.#buildResponse({
208+
this.#logger.error(`Tool "${toolName}" has not been registered.`);
209+
return new BedrockFunctionResponse({
210+
body: `Error: tool "${toolName}" has not been registered.`,
211+
sessionAttributes,
212+
promptSessionAttributes,
213+
knowledgeBasesConfiguration,
214+
}).build({
179215
actionGroup,
180-
function: toolName,
181-
body: 'Error: tool has not been registered in handler.',
216+
func: toolName,
182217
});
183218
}
184219

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

206241
try {
207-
const res = await tool.handler(toolParams, { event, context });
208-
const body = res == null ? '' : JSON.stringify(res);
209-
return this.#buildResponse({
210-
actionGroup,
211-
function: toolName,
242+
const response = await tool.handler(toolParams, { event, context });
243+
if (response instanceof BedrockFunctionResponse) {
244+
return response.build({
245+
actionGroup,
246+
func: toolName,
247+
});
248+
}
249+
const body =
250+
isNullOrUndefined(response) || response === ''
251+
? ''
252+
: JSON.stringify(response);
253+
return new BedrockFunctionResponse({
212254
body,
213255
sessionAttributes,
214256
promptSessionAttributes,
257+
knowledgeBasesConfiguration,
258+
}).build({
259+
actionGroup,
260+
func: toolName,
215261
});
216262
} catch (error) {
217263
this.#logger.error(`An error occurred in tool ${toolName}.`, error);
218-
return this.#buildResponse({
219-
actionGroup,
220-
function: toolName,
221-
body: `Error when invoking tool: ${error}`,
264+
const errorMessage =
265+
error instanceof Error
266+
? `${error.name} - ${error.message}`
267+
: String(error);
268+
return new BedrockFunctionResponse({
269+
body: `Unable to complete tool execution due to ${errorMessage}`,
222270
sessionAttributes,
223271
promptSessionAttributes,
272+
knowledgeBasesConfiguration,
273+
}).build({
274+
actionGroup,
275+
func: toolName,
224276
});
225277
}
226278
}
227279
}
280+
281+
export { BedrockAgentFunctionResolver };

0 commit comments

Comments
 (0)