Skip to content

Commit 720ddcb

Browse files
authored
feat(event-handler): add Amazon Bedrock Agents Functions Resolver (#3957)
1 parent ddfb66c commit 720ddcb

12 files changed

+1104
-21
lines changed

packages/event-handler/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@
3939
"default": "./lib/esm/appsync-events/index.js"
4040
}
4141
},
42+
"./bedrock-agent": {
43+
"require": {
44+
"types": "./lib/cjs/bedrock-agent/index.d.ts",
45+
"default": "./lib/cjs/bedrock-agent/index.js"
46+
},
47+
"import": {
48+
"types": "./lib/esm/bedrock-agent/index.d.ts",
49+
"default": "./lib/esm/bedrock-agent/index.js"
50+
}
51+
},
4252
"./types": {
4353
"require": {
4454
"types": "./lib/cjs/types/index.d.ts",
@@ -56,6 +66,10 @@
5666
"./lib/cjs/appsync-events/index.d.ts",
5767
"./lib/esm/appsync-events/index.d.ts"
5868
],
69+
"bedrock-agent": [
70+
"./lib/cjs/bedrock-agent/index.d.ts",
71+
"./lib/esm/bedrock-agent/index.d.ts"
72+
],
5973
"types": [
6074
"./lib/cjs/types/index.d.ts",
6175
"./lib/esm/types/index.d.ts"
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import { EnvironmentVariablesService } from '@aws-lambda-powertools/commons';
2+
import type { Context } from 'aws-lambda';
3+
import type {
4+
BedrockAgentFunctionResponse,
5+
Configuration,
6+
ParameterValue,
7+
ResolverOptions,
8+
ResponseOptions,
9+
Tool,
10+
ToolFunction,
11+
} from '../types/bedrock-agent.js';
12+
import type { GenericLogger } from '../types/common.js';
13+
import { assertBedrockAgentFunctionEvent } from './utils.js';
14+
15+
export class BedrockAgentFunctionResolver {
16+
readonly #tools: Map<string, Tool> = new Map();
17+
readonly #envService: EnvironmentVariablesService;
18+
readonly #logger: Pick<GenericLogger, 'debug' | 'warn' | 'error'>;
19+
20+
constructor(options?: ResolverOptions) {
21+
this.#envService = new EnvironmentVariablesService();
22+
const alcLogLevel = this.#envService.get('AWS_LAMBDA_LOG_LEVEL');
23+
this.#logger = options?.logger ?? {
24+
debug: alcLogLevel === 'DEBUG' ? console.debug : () => {},
25+
error: console.error,
26+
warn: console.warn,
27+
};
28+
}
29+
30+
/**
31+
* Register a tool function for the Bedrock Agent.
32+
*
33+
* This method registers a function that can be invoked by a Bedrock Agent.
34+
*
35+
* @example
36+
* ```ts
37+
* import { BedrockAgentFunctionResolver } from '@aws-lambda-powertools/event-handler/bedrock-agent-function';
38+
*
39+
* const app = new BedrockAgentFunctionResolver();
40+
*
41+
* app.tool(async (params) => {
42+
* const { name } = params;
43+
* return `Hello, ${name}!`;
44+
* }, {
45+
* name: 'greeting',
46+
* description: 'Greets a person by name',
47+
* });
48+
*
49+
* export const handler = async (event, context) =>
50+
* app.resolve(event, context);
51+
* ```
52+
*
53+
* The method also works as a class method decorator:
54+
*
55+
* @example
56+
* ```ts
57+
* import { BedrockAgentFunctionResolver } from '@aws-lambda-powertools/event-handler/bedrock-agent-function';
58+
*
59+
* const app = new BedrockAgentFunctionResolver();
60+
*
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+
* }
67+
*
68+
* async handler(event, context) {
69+
* return app.resolve(event, context);
70+
* }
71+
* }
72+
*
73+
* const lambda = new Lambda();
74+
* export const handler = lambda.handler.bind(lambda);
75+
* ```
76+
*
77+
* @param fn - The tool function
78+
* @param config - The configuration object for the tool
79+
*/
80+
public tool<TParams extends Record<string, ParameterValue>>(
81+
fn: ToolFunction<TParams>,
82+
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 {
109+
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+
118+
if (this.#tools.has(name)) {
119+
this.#logger.warn(
120+
`Tool ${name} already registered. Overwriting with new definition.`
121+
);
122+
}
123+
124+
this.#tools.set(name, {
125+
handler: handler as ToolFunction,
126+
config,
127+
});
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+
};
158+
}
159+
160+
async resolve(
161+
event: unknown,
162+
context: Context
163+
): Promise<BedrockAgentFunctionResponse> {
164+
assertBedrockAgentFunctionEvent(event);
165+
166+
const {
167+
function: toolName,
168+
parameters = [],
169+
actionGroup,
170+
sessionAttributes,
171+
promptSessionAttributes,
172+
} = event;
173+
174+
const tool = this.#tools.get(toolName);
175+
176+
if (tool == null) {
177+
this.#logger.error(`Tool ${toolName} has not been registered.`);
178+
return this.#buildResponse({
179+
actionGroup,
180+
function: toolName,
181+
body: 'Error: tool has not been registered in handler.',
182+
});
183+
}
184+
185+
const toolParams: Record<string, ParameterValue> = {};
186+
for (const param of parameters) {
187+
switch (param.type) {
188+
case 'boolean': {
189+
toolParams[param.name] = param.value === 'true';
190+
break;
191+
}
192+
case 'number':
193+
case 'integer': {
194+
toolParams[param.name] = Number(param.value);
195+
break;
196+
}
197+
// this default will also catch array types but we leave them as strings
198+
// because we cannot reliably parse them
199+
default: {
200+
toolParams[param.name] = param.value;
201+
break;
202+
}
203+
}
204+
}
205+
206+
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,
212+
body,
213+
sessionAttributes,
214+
promptSessionAttributes,
215+
});
216+
} catch (error) {
217+
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}`,
222+
sessionAttributes,
223+
promptSessionAttributes,
224+
});
225+
}
226+
}
227+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { BedrockAgentFunctionResolver } from './BedrockAgentFunctionResolver.js';
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { isRecord, isString } from '@aws-lambda-powertools/commons/typeutils';
2+
import type { BedrockAgentFunctionEvent } from '../types/bedrock-agent.js';
3+
4+
/**
5+
* Asserts that the provided event is a BedrockAgentFunctionEvent.
6+
*
7+
* @param event - The incoming event to check
8+
* @throws Error if the event is not a valid BedrockAgentFunctionEvent
9+
*/
10+
export function assertBedrockAgentFunctionEvent(
11+
event: unknown
12+
): asserts event is BedrockAgentFunctionEvent {
13+
const isValid =
14+
isRecord(event) &&
15+
'actionGroup' in event &&
16+
isString(event.actionGroup) &&
17+
'function' in event &&
18+
isString(event.function) &&
19+
(!('parameters' in event) ||
20+
(Array.isArray(event.parameters) &&
21+
event.parameters.every(
22+
(param) =>
23+
isRecord(param) &&
24+
'name' in param &&
25+
isString(param.name) &&
26+
'type' in param &&
27+
isString(param.type) &&
28+
'value' in param &&
29+
isString(param.value)
30+
))) &&
31+
'messageVersion' in event &&
32+
isString(event.messageVersion) &&
33+
'agent' in event &&
34+
isRecord(event.agent) &&
35+
'name' in event.agent &&
36+
isString(event.agent.name) &&
37+
'id' in event.agent &&
38+
isString(event.agent.id) &&
39+
'alias' in event.agent &&
40+
isString(event.agent.alias) &&
41+
'version' in event.agent &&
42+
isString(event.agent.version) &&
43+
'inputText' in event &&
44+
isString(event.inputText) &&
45+
'sessionId' in event &&
46+
isString(event.sessionId) &&
47+
'sessionAttributes' in event &&
48+
isRecord(event.sessionAttributes) &&
49+
'promptSessionAttributes' in event &&
50+
isRecord(event.promptSessionAttributes);
51+
52+
if (!isValid) {
53+
throw new Error('Event is not a valid BedrockAgentFunctionEvent');
54+
}
55+
}

packages/event-handler/src/types/appsync-events.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,7 @@
11
import type { Context } from 'aws-lambda';
22
import type { RouteHandlerRegistry } from '../appsync-events/RouteHandlerRegistry.js';
33
import type { Router } from '../appsync-events/Router.js';
4-
5-
// #region Shared
6-
7-
// biome-ignore lint/suspicious/noExplicitAny: We intentionally use `any` here to represent any type of data and keep the logger is as flexible as possible.
8-
type Anything = any;
9-
10-
/**
11-
* Interface for a generic logger object.
12-
*/
13-
type GenericLogger = {
14-
trace?: (...content: Anything[]) => void;
15-
debug: (...content: Anything[]) => void;
16-
info?: (...content: Anything[]) => void;
17-
warn: (...content: Anything[]) => void;
18-
error: (...content: Anything[]) => void;
19-
};
4+
import type { Anything, GenericLogger } from './common.js';
205

216
// #region OnPublish fn
227

0 commit comments

Comments
 (0)