From 8eee45d19e777896b733776cebde1de03340f76d Mon Sep 17 00:00:00 2001 From: svozza Date: Thu, 22 May 2025 13:58:49 +0100 Subject: [PATCH 01/17] wip(event-handler): add Amazon Bedrock Agents Functions Resolver --- packages/event-handler/package.json | 14 + .../BedrockAgentFunctionResolver.ts | 196 ++++++++ .../src/bedrock-agent-function/index.ts | 1 + .../src/bedrock-agent-function/utils.ts | 14 + .../event-handler/src/types/appsync-events.ts | 17 +- .../src/types/bedrock-agent-function.ts | 105 ++++ packages/event-handler/src/types/common.ts | 15 + packages/event-handler/src/types/index.ts | 19 + .../unit/BedrockAgentFunctionResolver.test.ts | 457 ++++++++++++++++++ 9 files changed, 822 insertions(+), 16 deletions(-) create mode 100644 packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts create mode 100644 packages/event-handler/src/bedrock-agent-function/index.ts create mode 100644 packages/event-handler/src/bedrock-agent-function/utils.ts create mode 100644 packages/event-handler/src/types/bedrock-agent-function.ts create mode 100644 packages/event-handler/src/types/common.ts create mode 100644 packages/event-handler/tests/unit/BedrockAgentFunctionResolver.test.ts diff --git a/packages/event-handler/package.json b/packages/event-handler/package.json index c99c3f4486..79256a2a2e 100644 --- a/packages/event-handler/package.json +++ b/packages/event-handler/package.json @@ -39,6 +39,16 @@ "default": "./lib/esm/appsync-events/index.js" } }, + "./bedrock-agent-function": { + "require": { + "types": "./lib/cjs/bedrock-agent-function/index.d.ts", + "default": "./lib/cjs/bedrock-agent-function/index.js" + }, + "import": { + "types": "./lib/esm/bedrock-agent-function/index.d.ts", + "default": "./lib/esm/bedrock-agent-function/index.js" + } + }, "./types": { "require": { "types": "./lib/cjs/types/index.d.ts", @@ -56,6 +66,10 @@ "./lib/cjs/appsync-events/index.d.ts", "./lib/esm/appsync-events/index.d.ts" ], + "bedrock-agent-function": [ + "./lib/cjs/bedrock-agent-function/index.d.ts", + "./lib/esm/bedrock-agent-function/index.d.ts" + ], "types": [ "./lib/cjs/types/index.d.ts", "./lib/esm/types/index.d.ts" diff --git a/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts b/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts new file mode 100644 index 0000000000..32927dcbab --- /dev/null +++ b/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts @@ -0,0 +1,196 @@ +import { EnvironmentVariablesService } from '@aws-lambda-powertools/commons'; +import type { Context } from 'aws-lambda'; +import type { + BedrockAgentFunctionEvent, + BedrockAgentFunctionResponse, + Configuration, + GenericLogger, + ResolverOptions, + ResponseOptions, + Tool, + ToolFunction, +} from '../types/index.js'; +import { isPrimitive } from './utils.js'; + +export class BedrockAgentFunctionResolver { + readonly #tools: Map = new Map(); + readonly #envService: EnvironmentVariablesService; + readonly #logger: Pick; + + constructor(options?: ResolverOptions) { + this.#envService = new EnvironmentVariablesService(); + const alcLogLevel = this.#envService.get('AWS_LAMBDA_LOG_LEVEL'); + this.#logger = options?.logger ?? { + debug: alcLogLevel === 'DEBUG' ? console.debug : () => {}, + error: console.error, + warn: console.warn, + }; + } + + /** + * Register a tool function for the Bedrock Agent. + * + * This method registers a function that can be invoked by a Bedrock Agent. + * + * @example + * ```ts + * import { BedrockAgentFunctionResolver } from '@aws-lambda-powertools/event-handler/bedrock-agent-function'; + * + * const app = new BedrockAgentFunctionResolver(); + * + * app.tool(async (params) => { + * const { name } = params; + * return `Hello, ${name}!`; + * }, { + * name: 'greeting', + * definition: 'Greets a person by name', + * }); + * + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * + * The method also works as a class method decorator: + * + * @example + * ```ts + * import { BedrockAgentFunctionResolver } from '@aws-lambda-powertools/event-handler/bedrock-agent-function'; + * + * const app = new BedrockAgentFunctionResolver(); + * + * class Lambda { + * @app.tool({ name: 'greeting', definition: 'Greets a person by name' }) + * async greeting(params) { + * const { name } = params; + * return `Hello, ${name}!`; + * } + * + * async handler(event, context) { + * return app.resolve(event, context); + * } + * } + * + * const lambda = new Lambda(); + * export const handler = lambda.handler.bind(lambda); + * ``` + * + * @param fn - The tool function + * @param config - The configuration object for the tool + */ + public tool(fn: ToolFunction, config: Configuration): void; + public tool(config: Configuration): MethodDecorator; + public tool( + fnOrConfig: ToolFunction | Configuration, + config?: Configuration + ): MethodDecorator | void { + // When used as a method (not a decorator) + if (typeof fnOrConfig === 'function') { + return this.#registerTool(fnOrConfig, config as Configuration); + } + + // When used as a decorator + return (_target, _propertyKey, descriptor: PropertyDescriptor) => { + const toolFn = descriptor.value as ToolFunction; + this.#registerTool(toolFn, fnOrConfig); + return descriptor; + }; + } + + #registerTool(fn: ToolFunction, config: Configuration): void { + 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.` + ); + } + + this.#tools.set(name, { function: fn, 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, + }; + } + + async resolve( + event: BedrockAgentFunctionEvent, + context: Context + ): Promise { + const { + function: toolName, + parameters = [], + actionGroup, + sessionAttributes, + promptSessionAttributes, + } = event; + + const tool = this.#tools.get(toolName); + + if (tool == null) { + this.#logger.error(`Tool ${toolName} has not been registered.`); + return this.#buildResponse({ + actionGroup, + function: toolName, + body: 'Error: tool has not been registered in handler.', + }); + } + + const parameterObject: Record = Object.fromEntries( + parameters.map((param) => [param.name, param.value]) + ); + + try { + const res = (await tool.function(parameterObject)) ?? ''; + const body = isPrimitive(res) ? String(res) : JSON.stringify(res); + return this.#buildResponse({ + actionGroup, + function: toolName, + body, + sessionAttributes, + promptSessionAttributes, + }); + } catch (error) { + this.#logger.error(`An error occurred in tool ${toolName}.`, error); + return this.#buildResponse({ + actionGroup, + function: toolName, + body: `Error when invoking tool: ${error}`, + sessionAttributes, + promptSessionAttributes, + }); + } + } +} diff --git a/packages/event-handler/src/bedrock-agent-function/index.ts b/packages/event-handler/src/bedrock-agent-function/index.ts new file mode 100644 index 0000000000..a18e9dd726 --- /dev/null +++ b/packages/event-handler/src/bedrock-agent-function/index.ts @@ -0,0 +1 @@ +export { BedrockAgentFunctionResolver } from './BedrockAgentFunctionResolver.js'; diff --git a/packages/event-handler/src/bedrock-agent-function/utils.ts b/packages/event-handler/src/bedrock-agent-function/utils.ts new file mode 100644 index 0000000000..525c9f89fb --- /dev/null +++ b/packages/event-handler/src/bedrock-agent-function/utils.ts @@ -0,0 +1,14 @@ +import type { + JSONPrimitive, + JSONValue, +} from '@aws-lambda-powertools/commons/types'; + +export function isPrimitive(value: JSONValue): value is JSONPrimitive { + return ( + value === null || + value === undefined || + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ); +} diff --git a/packages/event-handler/src/types/appsync-events.ts b/packages/event-handler/src/types/appsync-events.ts index 7c391f3312..1367cacd93 100644 --- a/packages/event-handler/src/types/appsync-events.ts +++ b/packages/event-handler/src/types/appsync-events.ts @@ -1,22 +1,7 @@ import type { Context } from 'aws-lambda'; import type { RouteHandlerRegistry } from '../appsync-events/RouteHandlerRegistry.js'; import type { Router } from '../appsync-events/Router.js'; - -// #region Shared - -// 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. -type Anything = any; - -/** - * Interface for a generic logger object. - */ -type GenericLogger = { - trace?: (...content: Anything[]) => void; - debug: (...content: Anything[]) => void; - info?: (...content: Anything[]) => void; - warn: (...content: Anything[]) => void; - error: (...content: Anything[]) => void; -}; +import type { Anything, GenericLogger } from './common.js'; // #region OnPublish fn diff --git a/packages/event-handler/src/types/bedrock-agent-function.ts b/packages/event-handler/src/types/bedrock-agent-function.ts new file mode 100644 index 0000000000..b3f1037fd5 --- /dev/null +++ b/packages/event-handler/src/types/bedrock-agent-function.ts @@ -0,0 +1,105 @@ +import type { JSONValue } from '@aws-lambda-powertools/commons/types'; +import type { GenericLogger } from '../types/common.js'; + +type Configuration = { + name: string; + definition: string; +}; + +type Parameter = { + name: string; + type: 'string' | 'number' | 'integer' | 'boolean' | 'array'; + value: string; +}; + +type ParameterPrimitives = string | number | boolean + +type ParameterValue = ParameterPrimitives | Array + +type Tool> = { + // biome-ignore lint/suspicious/noConfusingVoidType: we need to support async functions that don't have an explicit return value + function: (params: TParams) => Promise; + config: Configuration; +}; + +type ToolFunction = Tool['function']; + +type Attributes = Record; + +type FunctionIdentifier = { + actionGroup: string; + function: string; +}; + +type FunctionInvocation = FunctionIdentifier & { + parameters?: Array; +}; + +type BedrockAgentFunctionEvent = FunctionInvocation & { + messageVersion: string; + agent: { + name: string; + id: string; + alias: string; + version: string; + }; + inputText: string; + sessionId: string; + sessionAttributes: Attributes; + promptSessionAttributes: Attributes; +}; + +type ResponseState = 'ERROR' | 'REPROMPT'; + +type TextResponseBody = { + TEXT: { + body: string; + }; +}; + +type SessionData = { + sessionAttributes?: Attributes; + promptSessionAttributes?: Attributes; +}; + +type BedrockAgentFunctionResponse = SessionData & { + messageVersion: string; + response: FunctionIdentifier & { + functionResponse: { + responseState?: ResponseState; + responseBody: TextResponseBody; + }; + }; +}; + +type ResponseOptions = FunctionIdentifier & + SessionData & { + body: string; + errorType?: ResponseState; + }; + +/** + * Options for the {@link BedrockAgentFunctionResolver} class + */ +type ResolverOptions = { + /** + * 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. + */ + logger?: GenericLogger; +}; + +export type { + Configuration, + Tool, + ToolFunction, + Parameter, + Attributes, + FunctionIdentifier, + FunctionInvocation, + BedrockAgentFunctionEvent, + BedrockAgentFunctionResponse, + ResponseOptions, + ResolverOptions, +}; diff --git a/packages/event-handler/src/types/common.ts b/packages/event-handler/src/types/common.ts new file mode 100644 index 0000000000..a3f71b397b --- /dev/null +++ b/packages/event-handler/src/types/common.ts @@ -0,0 +1,15 @@ +// 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. +type Anything = any; + +/** + * Interface for a generic logger object. + */ +type GenericLogger = { + trace?: (...content: Anything[]) => void; + debug: (...content: Anything[]) => void; + info?: (...content: Anything[]) => void; + warn: (...content: Anything[]) => void; + error: (...content: Anything[]) => void; +}; + +export type { Anything, GenericLogger }; diff --git a/packages/event-handler/src/types/index.ts b/packages/event-handler/src/types/index.ts index 424189e05c..c0fe3d44e7 100644 --- a/packages/event-handler/src/types/index.ts +++ b/packages/event-handler/src/types/index.ts @@ -8,3 +8,22 @@ export type { RouteOptions, RouterOptions, } from './appsync-events.js'; + +export type { + Configuration, + Tool, + ToolFunction, + Parameter, + Attributes, + FunctionIdentifier, + FunctionInvocation, + BedrockAgentFunctionEvent, + BedrockAgentFunctionResponse, + ResponseOptions, + ResolverOptions, +} from './bedrock-agent-function.js'; + +export type { + GenericLogger, + Anything, +} from './common.js'; diff --git a/packages/event-handler/tests/unit/BedrockAgentFunctionResolver.test.ts b/packages/event-handler/tests/unit/BedrockAgentFunctionResolver.test.ts new file mode 100644 index 0000000000..8a7386bf0a --- /dev/null +++ b/packages/event-handler/tests/unit/BedrockAgentFunctionResolver.test.ts @@ -0,0 +1,457 @@ +import context from '@aws-lambda-powertools/testing-utils/context'; +import type { Context } from 'aws-lambda'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { BedrockAgentFunctionEvent, Parameter } from '../../src/types'; +import { BedrockAgentFunctionResolver } from '../../src/bedrock-agent-function'; + +function createEvent(functionName: string, parameters?: Parameter[]) { + return { + messageVersion: '1.0', + agent: { + name: 'agentName', + id: 'agentId', + alias: 'agentAlias', + version: '1', + }, + sessionId: 'sessionId', + inputText: 'inputText', + function: functionName, + parameters, + actionGroup: 'myActionGroup', + sessionAttributes: {}, + promptSessionAttributes: {}, + }; +} + +describe('Class: BedrockAgentFunctionResolver', () => { + beforeEach(() => { + vi.unstubAllEnvs(); + }); + + it('uses a default logger with only warnings if none is provided', () => { + // Prepare + const app = new BedrockAgentFunctionResolver(); + + app.tool(async (_params) => {}, { + name: 'noop', + definition: 'Does nothing', + }); + + // Assess + expect(console.debug).not.toHaveBeenCalled(); + }); + + it('emits debug message when AWS_LAMBDA_LOG_LEVEL is set to DEBUG', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_LOG_LEVEL', 'DEBUG'); + const app = new BedrockAgentFunctionResolver(); + + app.tool(async (_params) => {}, { + name: 'noop', + definition: 'Does nothing', + }); + + // Assess + expect(console.debug).toHaveBeenCalled(); + }); + + it('accepts custom logger', async () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_LOG_LEVEL', 'DEBUG'); + + const logger = { + debug: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + const app = new BedrockAgentFunctionResolver({ logger }); + + app.tool(async (_params) => {}, { + name: 'noop', + definition: 'Does nothing', + }); + + await app.resolve(createEvent('noop', []), context); + + // Assess + expect(logger.debug).toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalled(); + }); + + it('only allows five tools to be registered', async () => { + // Prepare + const app = new BedrockAgentFunctionResolver(); + + for (const num of [1, 2, 3, 4, 5]) { + app.tool(async (_params) => {}, { + name: `noop${num}`, + definition: 'Does nothing', + }); + } + + app.tool(async (params) => { + return Number(params.a) + Number(params); + }, { + name: 'mult', + definition: 'Multiplies two numbers', + }); + + expect(console.warn).toHaveBeenLastCalledWith( + 'The maximum number of tools that can be registered is 5. Tool mult will not be registered.' + ); + + const event = createEvent('mult', [ + { + name: 'a', + type: 'number', + value: '1', + }, + { + name: 'b', + type: 'number', + value: '2', + }, + ]); + + const actual = await app.resolve(event, context); + + expect(actual.response.function).toEqual('mult'); + expect(actual.response.functionResponse.responseBody.TEXT.body).toEqual( + 'Error: tool has not been registered in handler.' + ); + }); + + it('overwrites tools with the same name and uses the latest definition', async () => { + // Prepare + const app = new BedrockAgentFunctionResolver(); + + const event = createEvent('math', [ + { + name: 'a', + type: 'number', + value: '10', + }, + { + name: 'b', + type: 'number', + value: '2', + }, + ]); + + app.tool( + async (params) => { + return Number(params.a) + Number(params.b); + }, + { + name: 'math', + definition: 'Adds two numbers', + } + ); + + const addResult = await app.resolve(event, context); + expect(addResult.response.function).toEqual('math'); + expect(addResult.response.functionResponse.responseBody.TEXT.body).toEqual( + '12' + ); + + app.tool( + async (params) => { + return Number(params.a) * Number(params.b); + }, + { + name: 'math', + definition: 'Multiplies two numbers', + } + ); + + const multiplyResult = await app.resolve(event, context); + expect(multiplyResult.response.function).toEqual('math'); + expect( + multiplyResult.response.functionResponse.responseBody.TEXT.body + ).toEqual('20'); + + app.tool( + async (params) => { + return Number(params.a) / Number(params.b); + }, + { + name: 'math', + definition: 'Divides two numbers', + } + ); + + const divideResult = await app.resolve(event, context); + expect(divideResult.response.function).toEqual('math'); + expect( + divideResult.response.functionResponse.responseBody.TEXT.body + ).toEqual('5'); + + expect(console.warn).toHaveBeenCalledTimes(3); + expect(console.warn).toHaveBeenCalledWith( + 'Tool math already registered. Overwriting with new definition.' + ); + }); + + it('can be invoked using the decorator pattern', async () => { + // Prepare + const app = new BedrockAgentFunctionResolver(); + + class Lambda { + @app.tool({ name: 'hello', definition: 'Says hello' }) + async helloWorld() { + return 'Hello, world!'; + } + + @app.tool({ name: 'add', definition: 'Adds two numbers' }) + async add(params: { a: string; b: string }) { + const { a, b } = params; + return Number.parseInt(a) + Number.parseInt(b); + } + + public async handler(event: BedrockAgentFunctionEvent, context: Context) { + return app.resolve(event, context); + } + } + + const lambda = new Lambda(); + + const helloEvent = createEvent('hello'); + + const helloResult = await lambda.handler(helloEvent, context); + expect(helloResult.response.function).toEqual('hello'); + expect( + helloResult.response.functionResponse.responseBody.TEXT.body + ).toEqual('Hello, world!'); + + const addEvent = createEvent('add', [ + { + name: 'a', + type: 'number', + value: '1', + }, + { + name: 'b', + type: 'number', + value: '2', + }, + ]); + + const addResult = await lambda.handler(addEvent, context); + expect(addResult.response.function).toEqual('add'); + expect(addResult.response.functionResponse.responseBody.TEXT.body).toEqual( + '3' + ); + }); + + it('handles functions that return different primitive types', async () => { + // Prepare + const app = new BedrockAgentFunctionResolver(); + + app.tool(async () => 'Hello, world!', { + name: 'string-tool', + definition: 'Returns a string', + }); + + app.tool(async () => 42, { + name: 'number-tool', + definition: 'Returns a number', + }); + + app.tool(async () => true, { + name: 'boolean-tool', + definition: 'Returns a boolean', + }); + + const stringResult = await app.resolve(createEvent('string-tool'), context); + expect(stringResult.response.function).toEqual('string-tool'); + expect( + stringResult.response.functionResponse.responseBody.TEXT.body + ).toEqual('Hello, world!'); + + const numberResult = await app.resolve(createEvent('number-tool'), context); + expect(numberResult.response.function).toEqual('number-tool'); + expect( + numberResult.response.functionResponse.responseBody.TEXT.body + ).toEqual('42'); + + const booleanResult = await app.resolve( + createEvent('boolean-tool'), + context + ); + expect(booleanResult.response.function).toEqual('boolean-tool'); + expect( + booleanResult.response.functionResponse.responseBody.TEXT.body + ).toEqual('true'); + }); + + it('handles functions that return complex types (array and object)', async () => { + // Prepare + const app = new BedrockAgentFunctionResolver(); + + app.tool(async () => [1, 'two', false, null], { + name: 'array-tool', + definition: 'Returns an array', + }); + + app.tool( + async () => ({ + name: 'John Doe', + age: 30, + isActive: true, + address: { + street: '123 Main St', + city: 'Anytown', + }, + }), + { + name: 'object-tool', + definition: 'Returns an object', + } + ); + + const arrayResult = await app.resolve(createEvent('array-tool'), context); + expect(arrayResult.response.function).toEqual('array-tool'); + expect( + arrayResult.response.functionResponse.responseBody.TEXT.body + ).toEqual('[1,"two",false,null]'); + + const objectResult = await app.resolve(createEvent('object-tool'), context); + expect(objectResult.response.function).toEqual('object-tool'); + expect( + objectResult.response.functionResponse.responseBody.TEXT.body + ).toEqual( + '{"name":"John Doe","age":30,"isActive":true,"address":{"street":"123 Main St","city":"Anytown"}}' + ); + }); + + it('handles functions that return null or undefined by returning a string', async () => { + // Prepare + const app = new BedrockAgentFunctionResolver(); + + app.tool(async () => null, { + name: 'null-tool', + definition: 'Returns null', + }); + + app.tool(async () => undefined, { + name: 'undefined-tool', + definition: 'Returns undefined', + }); + + app.tool(async () => {}, { + name: 'no-return-tool', + definition: 'Has no return statement', + }); + + const nullResult = await app.resolve(createEvent('null-tool'), context); + expect(nullResult.response.function).toEqual('null-tool'); + expect(nullResult.response.functionResponse.responseBody.TEXT.body).toEqual( + '' + ); + + const undefinedResult = await app.resolve( + createEvent('undefined-tool'), + context + ); + expect(undefinedResult.response.function).toEqual('undefined-tool'); + expect( + undefinedResult.response.functionResponse.responseBody.TEXT.body + ).toEqual(''); + + const noReturnResult = await app.resolve( + createEvent('no-return-tool'), + context + ); + expect(noReturnResult.response.function).toEqual('no-return-tool'); + expect( + noReturnResult.response.functionResponse.responseBody.TEXT.body + ).toEqual(''); + }); + + it('handles functions that throw errors', async () => { + // Prepare + const app = new BedrockAgentFunctionResolver(); + + app.tool( + async () => { + throw new Error('Something went wrong'); + }, + { + name: 'error-tool', + definition: 'Throws an error', + } + ); + + const errorResult = await app.resolve( + createEvent('error-tool', []), + context + ); + expect(errorResult.response.function).toEqual('error-tool'); + expect( + errorResult.response.functionResponse.responseBody.TEXT.body + ).toEqual('Error when invoking tool: Error: Something went wrong'); + expect(console.error).toHaveBeenCalledWith( + 'An error occurred in tool error-tool.', + new Error('Something went wrong') + ); + }); + + it('returns a fully structured BedrockAgentFunctionResponse', async () => { + // Prepare + const app = new BedrockAgentFunctionResolver(); + + // Register a tool that returns a simple value + app.tool( + async (params) => { + return `Hello, ${params.name}!`; + }, + { + name: 'greeting', + definition: 'Greets a person by name', + } + ); + + // Define custom session attributes and parameters + const customSessionAttrs = { + sessionAttr: '12345', + }; + + const customPromptAttrs = { + promptAttr: 'promptAttr', + }; + + // Create a custom event with session attributes + const customEvent = { + ...createEvent('greeting', [ + { + name: 'name', + type: 'string', + value: 'John', + }, + ]), + actionGroup: 'actionGroup', + sessionAttributes: customSessionAttrs, + promptSessionAttributes: customPromptAttrs, + }; + + const result = await app.resolve(customEvent, context); + + expect(result).toEqual({ + messageVersion: '1.0', + response: { + actionGroup: 'actionGroup', + function: 'greeting', + functionResponse: { + responseBody: { + TEXT: { + body: 'Hello, John!', + }, + }, + }, + }, + sessionAttributes: customSessionAttrs, + promptSessionAttributes: customPromptAttrs, + }); + }); +}); From ac2289d08379f9f34a8f235d037a84ba86b1f13a Mon Sep 17 00:00:00 2001 From: svozza Date: Fri, 23 May 2025 11:42:27 +0100 Subject: [PATCH 02/17] use it.each for testing fucntion return type tests --- .../unit/BedrockAgentFunctionResolver.test.ts | 291 ++++++++---------- 1 file changed, 124 insertions(+), 167 deletions(-) diff --git a/packages/event-handler/tests/unit/BedrockAgentFunctionResolver.test.ts b/packages/event-handler/tests/unit/BedrockAgentFunctionResolver.test.ts index 8a7386bf0a..3d3401357b 100644 --- a/packages/event-handler/tests/unit/BedrockAgentFunctionResolver.test.ts +++ b/packages/event-handler/tests/unit/BedrockAgentFunctionResolver.test.ts @@ -1,8 +1,8 @@ import context from '@aws-lambda-powertools/testing-utils/context'; import type { Context } from 'aws-lambda'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { BedrockAgentFunctionEvent, Parameter } from '../../src/types'; import { BedrockAgentFunctionResolver } from '../../src/bedrock-agent-function'; +import type { BedrockAgentFunctionEvent, Parameter } from '../../src/types'; function createEvent(functionName: string, parameters?: Parameter[]) { return { @@ -71,6 +71,8 @@ describe('Class: BedrockAgentFunctionResolver', () => { definition: 'Does nothing', }); + // Act + await app.resolve(createEvent('noop', []), context); // Assess @@ -90,15 +92,14 @@ describe('Class: BedrockAgentFunctionResolver', () => { }); } - app.tool(async (params) => { - return Number(params.a) + Number(params); - }, { - name: 'mult', - definition: 'Multiplies two numbers', - }); - - expect(console.warn).toHaveBeenLastCalledWith( - 'The maximum number of tools that can be registered is 5. Tool mult will not be registered.' + app.tool( + async (params) => { + return Number(params.a) + Number(params); + }, + { + name: 'mult', + definition: 'Multiplies two numbers', + } ); const event = createEvent('mult', [ @@ -114,8 +115,13 @@ describe('Class: BedrockAgentFunctionResolver', () => { }, ]); + // Act const actual = await app.resolve(event, context); + // Assess + expect(console.warn).toHaveBeenLastCalledWith( + 'The maximum number of tools that can be registered is 5. Tool mult will not be registered.' + ); expect(actual.response.function).toEqual('mult'); expect(actual.response.functionResponse.responseBody.TEXT.body).toEqual( 'Error: tool has not been registered in handler.' @@ -170,27 +176,6 @@ describe('Class: BedrockAgentFunctionResolver', () => { expect( multiplyResult.response.functionResponse.responseBody.TEXT.body ).toEqual('20'); - - app.tool( - async (params) => { - return Number(params.a) / Number(params.b); - }, - { - name: 'math', - definition: 'Divides two numbers', - } - ); - - const divideResult = await app.resolve(event, context); - expect(divideResult.response.function).toEqual('math'); - expect( - divideResult.response.functionResponse.responseBody.TEXT.body - ).toEqual('5'); - - expect(console.warn).toHaveBeenCalledTimes(3); - expect(console.warn).toHaveBeenCalledWith( - 'Tool math already registered. Overwriting with new definition.' - ); }); it('can be invoked using the decorator pattern', async () => { @@ -216,14 +201,6 @@ describe('Class: BedrockAgentFunctionResolver', () => { const lambda = new Lambda(); - const helloEvent = createEvent('hello'); - - const helloResult = await lambda.handler(helloEvent, context); - expect(helloResult.response.function).toEqual('hello'); - expect( - helloResult.response.functionResponse.responseBody.TEXT.body - ).toEqual('Hello, world!'); - const addEvent = createEvent('add', [ { name: 'a', @@ -237,65 +214,43 @@ describe('Class: BedrockAgentFunctionResolver', () => { }, ]); - const addResult = await lambda.handler(addEvent, context); - expect(addResult.response.function).toEqual('add'); - expect(addResult.response.functionResponse.responseBody.TEXT.body).toEqual( - '3' - ); - }); - - it('handles functions that return different primitive types', async () => { - // Prepare - const app = new BedrockAgentFunctionResolver(); - - app.tool(async () => 'Hello, world!', { - name: 'string-tool', - definition: 'Returns a string', - }); + // Act + const actual = await lambda.handler(addEvent, context); - app.tool(async () => 42, { - name: 'number-tool', - definition: 'Returns a number', - }); - - app.tool(async () => true, { - name: 'boolean-tool', - definition: 'Returns a boolean', - }); - - const stringResult = await app.resolve(createEvent('string-tool'), context); - expect(stringResult.response.function).toEqual('string-tool'); - expect( - stringResult.response.functionResponse.responseBody.TEXT.body - ).toEqual('Hello, world!'); - - const numberResult = await app.resolve(createEvent('number-tool'), context); - expect(numberResult.response.function).toEqual('number-tool'); - expect( - numberResult.response.functionResponse.responseBody.TEXT.body - ).toEqual('42'); - - const booleanResult = await app.resolve( - createEvent('boolean-tool'), - context + // Assess + expect(actual.response.function).toEqual('add'); + expect(actual.response.functionResponse.responseBody.TEXT.body).toEqual( + '3' ); - expect(booleanResult.response.function).toEqual('boolean-tool'); - expect( - booleanResult.response.functionResponse.responseBody.TEXT.body - ).toEqual('true'); }); - it('handles functions that return complex types (array and object)', async () => { - // Prepare - const app = new BedrockAgentFunctionResolver(); - - app.tool(async () => [1, 'two', false, null], { - name: 'array-tool', - definition: 'Returns an array', - }); - - app.tool( - async () => ({ + it.each([ + { + toolFunction: async () => 'Hello, world', + toolParams: { + name: 'string', + definition: 'Returns string', + }, + expected: 'Hello, world', + }, + { + toolFunction: async () => 42, + toolParams: { + name: 'number', + definition: 'Returns number', + }, + expected: '42', + }, + { + toolFunction: async () => true, + toolParams: { + name: 'boolean', + definition: 'Returns boolean', + }, + expected: 'true', + }, + { + toolFunction: async () => ({ name: 'John Doe', age: 30, isActive: true, @@ -304,70 +259,73 @@ describe('Class: BedrockAgentFunctionResolver', () => { city: 'Anytown', }, }), - { - name: 'object-tool', + toolParams: { + name: 'object', definition: 'Returns an object', - } - ); - - const arrayResult = await app.resolve(createEvent('array-tool'), context); - expect(arrayResult.response.function).toEqual('array-tool'); - expect( - arrayResult.response.functionResponse.responseBody.TEXT.body - ).toEqual('[1,"two",false,null]'); - - const objectResult = await app.resolve(createEvent('object-tool'), context); - expect(objectResult.response.function).toEqual('object-tool'); - expect( - objectResult.response.functionResponse.responseBody.TEXT.body - ).toEqual( - '{"name":"John Doe","age":30,"isActive":true,"address":{"street":"123 Main St","city":"Anytown"}}' - ); - }); - - it('handles functions that return null or undefined by returning a string', async () => { - // Prepare - const app = new BedrockAgentFunctionResolver(); - - app.tool(async () => null, { - name: 'null-tool', - definition: 'Returns null', - }); - - app.tool(async () => undefined, { - name: 'undefined-tool', - definition: 'Returns undefined', - }); - - app.tool(async () => {}, { - name: 'no-return-tool', - definition: 'Has no return statement', - }); - - const nullResult = await app.resolve(createEvent('null-tool'), context); - expect(nullResult.response.function).toEqual('null-tool'); - expect(nullResult.response.functionResponse.responseBody.TEXT.body).toEqual( - '' - ); - - const undefinedResult = await app.resolve( - createEvent('undefined-tool'), - context - ); - expect(undefinedResult.response.function).toEqual('undefined-tool'); - expect( - undefinedResult.response.functionResponse.responseBody.TEXT.body - ).toEqual(''); - - const noReturnResult = await app.resolve( - createEvent('no-return-tool'), - context - ); - expect(noReturnResult.response.function).toEqual('no-return-tool'); - expect( - noReturnResult.response.functionResponse.responseBody.TEXT.body - ).toEqual(''); - }); + }, + expected: + '{"name":"John Doe","age":30,"isActive":true,"address":{"street":"123 Main St","city":"Anytown"}}', + }, + { + toolFunction: async () => [1, 'two', false, null], + toolParams: { + name: 'array', + definition: 'Returns an array', + }, + expected: '[1,"two",false,null]', + }, + ])( + 'handles function that returns $toolParams.name', + async ({ toolFunction, toolParams, expected }) => { + // Prepare + const app = new BedrockAgentFunctionResolver(); + + app.tool(toolFunction, toolParams); + + // Act + const actual = await app.resolve(createEvent(toolParams.name), context); + + // Asses + expect(actual.response.function).toEqual(toolParams.name); + expect(actual.response.functionResponse.responseBody.TEXT.body).toEqual( + expected + ); + } + ); + + it.each([ + { + toolFunction: async () => null, + toolParams: { + name: 'null', + definition: 'Returns null', + }, + }, + { + toolFunction: async () => void 0, + toolParams: { + name: 'undefined', + definition: 'Returns undefined', + }, + }, + ])( + 'handles functions that return $toolParams.name by returning an empty string', + async ({ toolFunction, toolParams }) => { + // Prepare + const app = new BedrockAgentFunctionResolver(); + + app.tool(toolFunction, toolParams); + + // Assess + const actual = await app.resolve(createEvent(toolParams.name), context); + + // Act + expect(actual.response.function).toEqual(toolParams.name); + expect(actual.response.functionResponse.responseBody.TEXT.body).toEqual( + '' + ); + } + ); it('handles functions that throw errors', async () => { // Prepare @@ -383,14 +341,14 @@ describe('Class: BedrockAgentFunctionResolver', () => { } ); - const errorResult = await app.resolve( - createEvent('error-tool', []), - context + // Act + const actual = await app.resolve(createEvent('error-tool', []), context); + + // Assess + expect(actual.response.function).toEqual('error-tool'); + expect(actual.response.functionResponse.responseBody.TEXT.body).toEqual( + 'Error when invoking tool: Error: Something went wrong' ); - expect(errorResult.response.function).toEqual('error-tool'); - expect( - errorResult.response.functionResponse.responseBody.TEXT.body - ).toEqual('Error when invoking tool: Error: Something went wrong'); expect(console.error).toHaveBeenCalledWith( 'An error occurred in tool error-tool.', new Error('Something went wrong') @@ -401,7 +359,6 @@ describe('Class: BedrockAgentFunctionResolver', () => { // Prepare const app = new BedrockAgentFunctionResolver(); - // Register a tool that returns a simple value app.tool( async (params) => { return `Hello, ${params.name}!`; @@ -412,7 +369,6 @@ describe('Class: BedrockAgentFunctionResolver', () => { } ); - // Define custom session attributes and parameters const customSessionAttrs = { sessionAttr: '12345', }; @@ -421,7 +377,6 @@ describe('Class: BedrockAgentFunctionResolver', () => { promptAttr: 'promptAttr', }; - // Create a custom event with session attributes const customEvent = { ...createEvent('greeting', [ { @@ -435,8 +390,10 @@ describe('Class: BedrockAgentFunctionResolver', () => { promptSessionAttributes: customPromptAttrs, }; + // Act const result = await app.resolve(customEvent, context); + // Assess expect(result).toEqual({ messageVersion: '1.0', response: { From cb56656fdd9974d5608208a8ecb83dc56779b0de Mon Sep 17 00:00:00 2001 From: svozza Date: Fri, 23 May 2025 11:47:09 +0100 Subject: [PATCH 03/17] create separate folders for agent/appsync resolver tests --- .../unit/{ => appsync-events}/AppSyncEventsResolver.test.ts | 4 ++-- .../unit/{ => appsync-events}/RouteHandlerRegistry.test.ts | 4 ++-- .../tests/unit/{ => appsync-events}/Router.test.ts | 2 +- .../{ => bedrock-agent}/BedrockAgentFunctionResolver.test.ts | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) rename packages/event-handler/tests/unit/{ => appsync-events}/AppSyncEventsResolver.test.ts (98%) rename packages/event-handler/tests/unit/{ => appsync-events}/RouteHandlerRegistry.test.ts (96%) rename packages/event-handler/tests/unit/{ => appsync-events}/Router.test.ts (97%) rename packages/event-handler/tests/unit/{ => bedrock-agent}/BedrockAgentFunctionResolver.test.ts (98%) diff --git a/packages/event-handler/tests/unit/AppSyncEventsResolver.test.ts b/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts similarity index 98% rename from packages/event-handler/tests/unit/AppSyncEventsResolver.test.ts rename to packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts index 8da6f88c02..d75bede42f 100644 --- a/packages/event-handler/tests/unit/AppSyncEventsResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts @@ -3,11 +3,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { AppSyncEventsResolver, UnauthorizedException, -} from '../../src/appsync-events/index.js'; +} from '../../../src/appsync-events/index.js'; import { onPublishEventFactory, onSubscribeEventFactory, -} from '../helpers/factories.js'; +} from '../../helpers/factories.js'; describe('Class: AppSyncEventsResolver', () => { beforeEach(() => { diff --git a/packages/event-handler/tests/unit/RouteHandlerRegistry.test.ts b/packages/event-handler/tests/unit/appsync-events/RouteHandlerRegistry.test.ts similarity index 96% rename from packages/event-handler/tests/unit/RouteHandlerRegistry.test.ts rename to packages/event-handler/tests/unit/appsync-events/RouteHandlerRegistry.test.ts index fc0431967b..03bc8fdbfe 100644 --- a/packages/event-handler/tests/unit/RouteHandlerRegistry.test.ts +++ b/packages/event-handler/tests/unit/appsync-events/RouteHandlerRegistry.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { RouteHandlerRegistry } from '../../src/appsync-events/RouteHandlerRegistry.js'; -import type { RouteHandlerOptions } from '../../src/types/appsync-events.js'; +import { RouteHandlerRegistry } from '../../../src/appsync-events/RouteHandlerRegistry.js'; +import type { RouteHandlerOptions } from '../../../src/types/appsync-events.js'; describe('Class: RouteHandlerRegistry', () => { class MockRouteHandlerRegistry extends RouteHandlerRegistry { diff --git a/packages/event-handler/tests/unit/Router.test.ts b/packages/event-handler/tests/unit/appsync-events/Router.test.ts similarity index 97% rename from packages/event-handler/tests/unit/Router.test.ts rename to packages/event-handler/tests/unit/appsync-events/Router.test.ts index 90fa2492a1..c9fa3d9382 100644 --- a/packages/event-handler/tests/unit/Router.test.ts +++ b/packages/event-handler/tests/unit/appsync-events/Router.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { Router } from '../../src/appsync-events/index.js'; +import { Router } from '../../../src/appsync-events/index.js'; describe('Class: Router', () => { beforeEach(() => { diff --git a/packages/event-handler/tests/unit/BedrockAgentFunctionResolver.test.ts b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts similarity index 98% rename from packages/event-handler/tests/unit/BedrockAgentFunctionResolver.test.ts rename to packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts index 3d3401357b..0093089b83 100644 --- a/packages/event-handler/tests/unit/BedrockAgentFunctionResolver.test.ts +++ b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts @@ -1,8 +1,8 @@ import context from '@aws-lambda-powertools/testing-utils/context'; import type { Context } from 'aws-lambda'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { BedrockAgentFunctionResolver } from '../../src/bedrock-agent-function'; -import type { BedrockAgentFunctionEvent, Parameter } from '../../src/types'; +import { BedrockAgentFunctionResolver } from '../../../src/bedrock-agent-function/index.js'; +import type { BedrockAgentFunctionEvent, Parameter } from '../../../src/types'; function createEvent(functionName: string, parameters?: Parameter[]) { return { From d60418839cccf58cfd0ef1ec78576e1f2281a9d5 Mon Sep 17 00:00:00 2001 From: svozza Date: Fri, 23 May 2025 16:01:27 +0100 Subject: [PATCH 04/17] fix param name from definition to description --- .../BedrockAgentFunctionResolver.ts | 15 ++++---- .../src/types/bedrock-agent-function.ts | 6 ++-- .../BedrockAgentFunctionResolver.test.ts | 36 +++++++++---------- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts b/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts index 32927dcbab..40f20ad46c 100644 --- a/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts +++ b/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts @@ -4,12 +4,12 @@ import type { BedrockAgentFunctionEvent, BedrockAgentFunctionResponse, Configuration, - GenericLogger, ResolverOptions, ResponseOptions, Tool, ToolFunction, -} from '../types/index.js'; +} from '../types/bedrock-agent-function.js'; +import type { GenericLogger } from '../types/common.js'; import { isPrimitive } from './utils.js'; export class BedrockAgentFunctionResolver { @@ -43,7 +43,7 @@ export class BedrockAgentFunctionResolver { * return `Hello, ${name}!`; * }, { * name: 'greeting', - * definition: 'Greets a person by name', + * description: 'Greets a person by name', * }); * * export const handler = async (event, context) => @@ -59,7 +59,7 @@ export class BedrockAgentFunctionResolver { * const app = new BedrockAgentFunctionResolver(); * * class Lambda { - * @app.tool({ name: 'greeting', definition: 'Greets a person by name' }) + * @app.tool({ name: 'greeting', description: 'Greets a person by name' }) * async greeting(params) { * const { name } = params; * return `Hello, ${name}!`; @@ -77,15 +77,16 @@ export class BedrockAgentFunctionResolver { * @param fn - The tool function * @param config - The configuration object for the tool */ - public tool(fn: ToolFunction, config: Configuration): void; + public tool(fn: ToolFunction, config: Configuration): undefined; public tool(config: Configuration): MethodDecorator; public tool( fnOrConfig: ToolFunction | Configuration, config?: Configuration - ): MethodDecorator | void { + ): MethodDecorator | undefined { // When used as a method (not a decorator) if (typeof fnOrConfig === 'function') { - return this.#registerTool(fnOrConfig, config as Configuration); + this.#registerTool(fnOrConfig, config as Configuration); + return; } // When used as a decorator diff --git a/packages/event-handler/src/types/bedrock-agent-function.ts b/packages/event-handler/src/types/bedrock-agent-function.ts index b3f1037fd5..896b0e64c1 100644 --- a/packages/event-handler/src/types/bedrock-agent-function.ts +++ b/packages/event-handler/src/types/bedrock-agent-function.ts @@ -3,7 +3,7 @@ import type { GenericLogger } from '../types/common.js'; type Configuration = { name: string; - definition: string; + description: string; }; type Parameter = { @@ -12,9 +12,9 @@ type Parameter = { value: string; }; -type ParameterPrimitives = string | number | boolean +type ParameterPrimitives = string | number | boolean; -type ParameterValue = ParameterPrimitives | Array +type ParameterValue = ParameterPrimitives | Array; type Tool> = { // biome-ignore lint/suspicious/noConfusingVoidType: we need to support async functions that don't have an explicit return value diff --git a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts index 0093089b83..854c72ac5d 100644 --- a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts +++ b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts @@ -34,7 +34,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { app.tool(async (_params) => {}, { name: 'noop', - definition: 'Does nothing', + description: 'Does nothing', }); // Assess @@ -48,7 +48,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { app.tool(async (_params) => {}, { name: 'noop', - definition: 'Does nothing', + description: 'Does nothing', }); // Assess @@ -68,7 +68,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { app.tool(async (_params) => {}, { name: 'noop', - definition: 'Does nothing', + description: 'Does nothing', }); // Act @@ -88,7 +88,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { for (const num of [1, 2, 3, 4, 5]) { app.tool(async (_params) => {}, { name: `noop${num}`, - definition: 'Does nothing', + description: 'Does nothing', }); } @@ -98,7 +98,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { }, { name: 'mult', - definition: 'Multiplies two numbers', + description: 'Multiplies two numbers', } ); @@ -151,7 +151,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { }, { name: 'math', - definition: 'Adds two numbers', + description: 'Adds two numbers', } ); @@ -167,7 +167,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { }, { name: 'math', - definition: 'Multiplies two numbers', + description: 'Multiplies two numbers', } ); @@ -183,12 +183,12 @@ describe('Class: BedrockAgentFunctionResolver', () => { const app = new BedrockAgentFunctionResolver(); class Lambda { - @app.tool({ name: 'hello', definition: 'Says hello' }) + @app.tool({ name: 'hello', description: 'Says hello' }) async helloWorld() { return 'Hello, world!'; } - @app.tool({ name: 'add', definition: 'Adds two numbers' }) + @app.tool({ name: 'add', description: 'Adds two numbers' }) async add(params: { a: string; b: string }) { const { a, b } = params; return Number.parseInt(a) + Number.parseInt(b); @@ -229,7 +229,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { toolFunction: async () => 'Hello, world', toolParams: { name: 'string', - definition: 'Returns string', + description: 'Returns string', }, expected: 'Hello, world', }, @@ -237,7 +237,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { toolFunction: async () => 42, toolParams: { name: 'number', - definition: 'Returns number', + description: 'Returns number', }, expected: '42', }, @@ -245,7 +245,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { toolFunction: async () => true, toolParams: { name: 'boolean', - definition: 'Returns boolean', + description: 'Returns boolean', }, expected: 'true', }, @@ -261,7 +261,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { }), toolParams: { name: 'object', - definition: 'Returns an object', + description: 'Returns an object', }, expected: '{"name":"John Doe","age":30,"isActive":true,"address":{"street":"123 Main St","city":"Anytown"}}', @@ -270,7 +270,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { toolFunction: async () => [1, 'two', false, null], toolParams: { name: 'array', - definition: 'Returns an array', + description: 'Returns an array', }, expected: '[1,"two",false,null]', }, @@ -298,14 +298,14 @@ describe('Class: BedrockAgentFunctionResolver', () => { toolFunction: async () => null, toolParams: { name: 'null', - definition: 'Returns null', + description: 'Returns null', }, }, { toolFunction: async () => void 0, toolParams: { name: 'undefined', - definition: 'Returns undefined', + description: 'Returns undefined', }, }, ])( @@ -337,7 +337,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { }, { name: 'error-tool', - definition: 'Throws an error', + description: 'Throws an error', } ); @@ -365,7 +365,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { }, { name: 'greeting', - definition: 'Greets a person by name', + description: 'Greets a person by name', } ); From 4054abd90789ed84177eeea48cf8e9cbd45e85c1 Mon Sep 17 00:00:00 2001 From: svozza Date: Sat, 24 May 2025 00:59:18 +0100 Subject: [PATCH 05/17] make tool method generic for type Record> --- .../BedrockAgentFunctionResolver.ts | 52 +++- .../src/types/bedrock-agent-function.ts | 14 +- .../BedrockAgentFunctionResolver.test.ts | 224 +++++++++++++++--- 3 files changed, 242 insertions(+), 48 deletions(-) diff --git a/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts b/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts index 40f20ad46c..0a77915ffb 100644 --- a/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts +++ b/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts @@ -4,6 +4,7 @@ import type { BedrockAgentFunctionEvent, BedrockAgentFunctionResponse, Configuration, + ParameterValue, ResolverOptions, ResponseOptions, Tool, @@ -13,7 +14,8 @@ import type { GenericLogger } from '../types/common.js'; import { isPrimitive } from './utils.js'; export class BedrockAgentFunctionResolver { - readonly #tools: Map = new Map(); + readonly #tools: Map>> = + new Map(); readonly #envService: EnvironmentVariablesService; readonly #logger: Pick; @@ -77,10 +79,15 @@ export class BedrockAgentFunctionResolver { * @param fn - The tool function * @param config - The configuration object for the tool */ - public tool(fn: ToolFunction, config: Configuration): undefined; - public tool(config: Configuration): MethodDecorator; - public tool( - fnOrConfig: ToolFunction | Configuration, + public tool>( + fn: ToolFunction, + config: Configuration + ): undefined; + public tool>( + config: Configuration + ): MethodDecorator; + public tool>( + fnOrConfig: ToolFunction | Configuration, config?: Configuration ): MethodDecorator | undefined { // When used as a method (not a decorator) @@ -97,7 +104,10 @@ export class BedrockAgentFunctionResolver { }; } - #registerTool(fn: ToolFunction, config: Configuration): void { + #registerTool>( + handler: ToolFunction, + config: Configuration + ): void { const { name } = config; if (this.#tools.size >= 5) { @@ -113,7 +123,10 @@ export class BedrockAgentFunctionResolver { ); } - this.#tools.set(name, { function: fn, config }); + this.#tools.set(name, { + handler: handler as ToolFunction>, + config, + }); this.#logger.debug(`Tool ${name} has been registered.`); } @@ -169,12 +182,29 @@ export class BedrockAgentFunctionResolver { }); } - const parameterObject: Record = Object.fromEntries( - parameters.map((param) => [param.name, param.value]) - ); + const toolParams: Record = {}; + for (const param of parameters) { + switch (param.type) { + case 'boolean': { + toolParams[param.name] = param.value === 'true'; + break; + } + case 'number': + case 'integer': { + toolParams[param.name] = Number(param.value); + break; + } + // this default will also catch array types but we leave them as strings + // because we cannot reliably parse them + default: { + toolParams[param.name] = param.value; + break; + } + } + } try { - const res = (await tool.function(parameterObject)) ?? ''; + const res = (await tool.handler(toolParams, event, context)) ?? ''; const body = isPrimitive(res) ? String(res) : JSON.stringify(res); return this.#buildResponse({ actionGroup, diff --git a/packages/event-handler/src/types/bedrock-agent-function.ts b/packages/event-handler/src/types/bedrock-agent-function.ts index 896b0e64c1..1384049e70 100644 --- a/packages/event-handler/src/types/bedrock-agent-function.ts +++ b/packages/event-handler/src/types/bedrock-agent-function.ts @@ -1,4 +1,5 @@ import type { JSONValue } from '@aws-lambda-powertools/commons/types'; +import type { Context } from 'aws-lambda'; import type { GenericLogger } from '../types/common.js'; type Configuration = { @@ -16,14 +17,18 @@ type ParameterPrimitives = string | number | boolean; type ParameterValue = ParameterPrimitives | Array; -type Tool> = { +type ToolFunction> = ( + params: TParams, + event: BedrockAgentFunctionEvent, + context: Context // biome-ignore lint/suspicious/noConfusingVoidType: we need to support async functions that don't have an explicit return value - function: (params: TParams) => Promise; +) => Promise; + +type Tool> = { + handler: ToolFunction; config: Configuration; }; -type ToolFunction = Tool['function']; - type Attributes = Record; type FunctionIdentifier = { @@ -96,6 +101,7 @@ export type { ToolFunction, Parameter, Attributes, + ParameterValue, FunctionIdentifier, FunctionInvocation, BedrockAgentFunctionEvent, diff --git a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts index 854c72ac5d..2a4f597768 100644 --- a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts +++ b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts @@ -2,7 +2,13 @@ import context from '@aws-lambda-powertools/testing-utils/context'; import type { Context } from 'aws-lambda'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { BedrockAgentFunctionResolver } from '../../../src/bedrock-agent-function/index.js'; -import type { BedrockAgentFunctionEvent, Parameter } from '../../../src/types'; +import type { + BedrockAgentFunctionEvent, + Configuration, + Parameter, + ParameterValue, + ToolFunction, +} from '../../../src/types/bedrock-agent-function.js'; function createEvent(functionName: string, parameters?: Parameter[]) { return { @@ -93,8 +99,8 @@ describe('Class: BedrockAgentFunctionResolver', () => { } app.tool( - async (params) => { - return Number(params.a) + Number(params); + async (params: { a: number; b: number }) => { + return params.a + params.b; }, { name: 'mult', @@ -146,8 +152,8 @@ describe('Class: BedrockAgentFunctionResolver', () => { ]); app.tool( - async (params) => { - return Number(params.a) + Number(params.b); + async (params: { a: number; b: number }) => { + return params.a + params.b; }, { name: 'math', @@ -162,8 +168,8 @@ describe('Class: BedrockAgentFunctionResolver', () => { ); app.tool( - async (params) => { - return Number(params.a) * Number(params.b); + async (params: { a: number; b: number }) => { + return params.a * params.b; }, { name: 'math', @@ -178,6 +184,32 @@ describe('Class: BedrockAgentFunctionResolver', () => { ).toEqual('20'); }); + it('tool function has access to the event variable', async () => { + // Prepare + const app = new BedrockAgentFunctionResolver(); + + app.tool( + async (_params, event) => { + return event; + }, + { + name: 'event-accessor', + description: 'Accesses the event object', + } + ); + + const event = createEvent('event-accessor'); + + // Act + const result = await app.resolve(event, context); + + // Assess + expect(result.response.function).toEqual('event-accessor'); + expect(result.response.functionResponse.responseBody.TEXT.body).toEqual( + JSON.stringify(event) + ); + }); + it('can be invoked using the decorator pattern', async () => { // Prepare const app = new BedrockAgentFunctionResolver(); @@ -189,9 +221,9 @@ describe('Class: BedrockAgentFunctionResolver', () => { } @app.tool({ name: 'add', description: 'Adds two numbers' }) - async add(params: { a: string; b: string }) { + async add(params: { a: number; b: number }) { const { a, b } = params; - return Number.parseInt(a) + Number.parseInt(b); + return a + b; } public async handler(event: BedrockAgentFunctionEvent, context: Context) { @@ -225,30 +257,6 @@ describe('Class: BedrockAgentFunctionResolver', () => { }); it.each([ - { - toolFunction: async () => 'Hello, world', - toolParams: { - name: 'string', - description: 'Returns string', - }, - expected: 'Hello, world', - }, - { - toolFunction: async () => 42, - toolParams: { - name: 'number', - description: 'Returns number', - }, - expected: '42', - }, - { - toolFunction: async () => true, - toolParams: { - name: 'boolean', - description: 'Returns boolean', - }, - expected: 'true', - }, { toolFunction: async () => ({ name: 'John Doe', @@ -327,6 +335,156 @@ describe('Class: BedrockAgentFunctionResolver', () => { } ); + it('correctly parses boolean parameters', async () => { + // Prepare + const toolFunction: ToolFunction<{ arg: boolean }> = async (params) => + params.arg; + + const toolParams: Configuration = { + name: 'boolean', + description: 'Handles boolean parameters', + }; + + const parameters: Parameter[] = [ + { name: 'arg', type: 'boolean', value: 'true' }, + ]; + + const app = new BedrockAgentFunctionResolver(); + app.tool(toolFunction, toolParams); + + //Act + const actual = await app.resolve( + createEvent(toolParams.name, parameters), + context + ); + + // Assess + expect(actual.response.function).toEqual(toolParams.name); + expect(actual.response.functionResponse.responseBody.TEXT.body).toEqual( + 'true' + ); + }); + + it('correctly parses number parameters', async () => { + // Prepare + const toolFunction: ToolFunction<{ arg: number }> = async (params) => + params.arg + 10; + + const toolParams: Configuration = { + name: 'number', + description: 'Handles number parameters', + }; + + const parameters: Parameter[] = [ + { name: 'arg', type: 'number', value: '42' }, + ]; + + const app = new BedrockAgentFunctionResolver(); + app.tool(toolFunction, toolParams); + + // Act + const actual = await app.resolve( + createEvent(toolParams.name, parameters), + context + ); + + // Assess + expect(actual.response.function).toEqual(toolParams.name); + expect(actual.response.functionResponse.responseBody.TEXT.body).toEqual( + '52' + ); + }); + + it('correctly parses integer parameters', async () => { + // Prepare + const toolFunction: ToolFunction<{ arg: number }> = async (params) => + params.arg + 10; + + const toolParams: Configuration = { + name: 'integer', + description: 'Handles integer parameters', + }; + + const parameters: Parameter[] = [ + { name: 'arg', type: 'integer', value: '37' }, + ]; + + const app = new BedrockAgentFunctionResolver(); + app.tool(toolFunction, toolParams); + + // Act + const actual = await app.resolve( + createEvent(toolParams.name, parameters), + context + ); + + // Assess + expect(actual.response.function).toEqual(toolParams.name); + expect(actual.response.functionResponse.responseBody.TEXT.body).toEqual( + '47' + ); + }); + + it('correctly parses string parameters', async () => { + // Prepare + const toolFunction: ToolFunction<{ arg: string }> = async (params) => + `String: ${params.arg}`; + + const toolParams: Configuration = { + name: 'string', + description: 'Handles string parameters', + }; + + const parameters: Parameter[] = [ + { name: 'arg', type: 'string', value: 'hello world' }, + ]; + + const app = new BedrockAgentFunctionResolver(); + app.tool(toolFunction, toolParams); + + // Act + const actual = await app.resolve( + createEvent(toolParams.name, parameters), + context + ); + + // Assess + expect(actual.response.function).toEqual(toolParams.name); + expect(actual.response.functionResponse.responseBody.TEXT.body).toEqual( + 'String: hello world' + ); + }); + + it('correctly parses array parameters', async () => { + // Prepare + const toolFunction: ToolFunction<{ arg: string }> = async (params) => + `Array as string: ${params.arg}`; + + const toolParams: Configuration = { + name: 'array', + description: 'Handles array parameters (as string)', + }; + + const parameters: Parameter[] = [ + { name: 'arg', type: 'array', value: '[1,2,3]' }, + ]; + + const app = new BedrockAgentFunctionResolver(); + app.tool(toolFunction, toolParams); + + // Act + const actual = await app.resolve( + createEvent(toolParams.name, parameters), + context + ); + + // Assess + expect(actual.response.function).toEqual(toolParams.name); + expect(actual.response.functionResponse.responseBody.TEXT.body).toEqual( + 'Array as string: [1,2,3]' + ); + }); + it('handles functions that throw errors', async () => { // Prepare const app = new BedrockAgentFunctionResolver(); From bf915d36508d566fba730d3e6988f7d9c0d18350 Mon Sep 17 00:00:00 2001 From: svozza Date: Sat, 24 May 2025 01:40:58 +0100 Subject: [PATCH 06/17] set type of event given to resolve as unknown and use type assertion to verify it a bedrock event --- .../BedrockAgentFunctionResolver.ts | 16 ++--- .../src/bedrock-agent-function/utils.ts | 58 +++++++++++++++++++ .../BedrockAgentFunctionResolver.test.ts | 45 ++++++++++++++ 3 files changed, 112 insertions(+), 7 deletions(-) diff --git a/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts b/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts index 0a77915ffb..1428842602 100644 --- a/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts +++ b/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts @@ -1,7 +1,8 @@ +// TODO: rename folder to just bedrock-agent + import { EnvironmentVariablesService } from '@aws-lambda-powertools/commons'; import type { Context } from 'aws-lambda'; import type { - BedrockAgentFunctionEvent, BedrockAgentFunctionResponse, Configuration, ParameterValue, @@ -11,11 +12,10 @@ import type { ToolFunction, } from '../types/bedrock-agent-function.js'; import type { GenericLogger } from '../types/common.js'; -import { isPrimitive } from './utils.js'; +import { assertBedrockAgentFunctionEvent, isPrimitive } from './utils.js'; export class BedrockAgentFunctionResolver { - readonly #tools: Map>> = - new Map(); + readonly #tools: Map = new Map(); readonly #envService: EnvironmentVariablesService; readonly #logger: Pick; @@ -124,7 +124,7 @@ export class BedrockAgentFunctionResolver { } this.#tools.set(name, { - handler: handler as ToolFunction>, + handler: handler as ToolFunction, config, }); this.#logger.debug(`Tool ${name} has been registered.`); @@ -160,9 +160,11 @@ export class BedrockAgentFunctionResolver { } async resolve( - event: BedrockAgentFunctionEvent, + event: unknown, context: Context ): Promise { + assertBedrockAgentFunctionEvent(event); + const { function: toolName, parameters = [], @@ -205,7 +207,7 @@ export class BedrockAgentFunctionResolver { try { const res = (await tool.handler(toolParams, event, context)) ?? ''; - const body = isPrimitive(res) ? String(res) : JSON.stringify(res); + const body = isPrimitive(res) ? String(res) : JSON.stringify(res); //TODO just use JSON.stringify return this.#buildResponse({ actionGroup, function: toolName, diff --git a/packages/event-handler/src/bedrock-agent-function/utils.ts b/packages/event-handler/src/bedrock-agent-function/utils.ts index 525c9f89fb..ccc6899f0a 100644 --- a/packages/event-handler/src/bedrock-agent-function/utils.ts +++ b/packages/event-handler/src/bedrock-agent-function/utils.ts @@ -2,6 +2,8 @@ import type { JSONPrimitive, JSONValue, } from '@aws-lambda-powertools/commons/types'; +import { isRecord, isString } from '@aws-lambda-powertools/commons/typeutils'; +import type { BedrockAgentFunctionEvent } from '../types/bedrock-agent-function.js'; export function isPrimitive(value: JSONValue): value is JSONPrimitive { return ( @@ -12,3 +14,59 @@ export function isPrimitive(value: JSONValue): value is JSONPrimitive { typeof value === 'boolean' ); } + +/** + * Asserts that the provided event is a BedrockAgentFunctionEvent. + * + * @param event - The incoming event to check + * @throws Error if the event is not a valid BedrockAgentFunctionEvent + */ +export function assertBedrockAgentFunctionEvent( + event: unknown +): asserts event is BedrockAgentFunctionEvent { + const isValid = + typeof event === 'object' && + event !== null && + isRecord(event) && + 'actionGroup' in event && + isString(event.actionGroup) && + 'function' in event && + isString(event.function) && + (!('parameters' in event) || + event.parameters == null || + (Array.isArray(event.parameters) && + event.parameters.every( + (param) => + isRecord(param) && + 'name' in param && + isString(param.name) && + 'type' in param && + isString(param.type) && + 'value' in param && + isString(param.value) + ))) && + 'messageVersion' in event && + isString(event.messageVersion) && + 'agent' in event && + isRecord(event.agent) && + 'name' in event.agent && + isString(event.agent.name) && + 'id' in event.agent && + isString(event.agent.id) && + 'alias' in event.agent && + isString(event.agent.alias) && + 'version' in event.agent && + isString(event.agent.version) && + 'inputText' in event && + isString(event.inputText) && + 'sessionId' in event && + isString(event.sessionId) && + 'sessionAttributes' in event && + isRecord(event.sessionAttributes) && + 'promptSessionAttributes' in event && + isRecord(event.promptSessionAttributes); + + if (!isValid) { + throw new Error('Event is not a valid BedrockAgentFunctionEvent'); + } +} diff --git a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts index 2a4f597768..55ad3af21f 100644 --- a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts +++ b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts @@ -34,6 +34,51 @@ describe('Class: BedrockAgentFunctionResolver', () => { vi.unstubAllEnvs(); }); + it.each([ + { + name: 'null event', + invalidEvent: null, + }, + { + name: 'missing required fields', + invalidEvent: { + function: 'test-tool', + }, + }, + { + name: 'invalid parameters structure', + invalidEvent: { + function: 'test-tool', + actionGroup: 'testGroup', + messageVersion: '1.0', + agent: { + name: 'agentName', + id: 'agentId', + alias: 'agentAlias', + version: '1', + }, + inputText: 'test input', + sessionId: 'session123', + parameters: 'not an array', + sessionAttributes: {}, + promptSessionAttributes: {}, + }, + }, + ])('throws when given an invalid event: $name', async ({ invalidEvent }) => { + // Prepare + const app = new BedrockAgentFunctionResolver(); + + app.tool(async () => 'test', { + name: 'test-tool', + description: 'Test tool', + }); + + // Act & Assert + await expect(app.resolve(invalidEvent, context)).rejects.toThrow( + 'Event is not a valid BedrockAgentFunctionEvent' + ); + }); + it('uses a default logger with only warnings if none is provided', () => { // Prepare const app = new BedrockAgentFunctionResolver(); From 30dd319561540d948ad8386300002945f59e0e42 Mon Sep 17 00:00:00 2001 From: svozza Date: Sat, 24 May 2025 01:46:52 +0100 Subject: [PATCH 07/17] fix custom logger test to trigger all levels of log messages --- .../BedrockAgentFunctionResolver.test.ts | 68 ++++++++++++------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts index 55ad3af21f..0698f06d1a 100644 --- a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts +++ b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts @@ -106,32 +106,6 @@ describe('Class: BedrockAgentFunctionResolver', () => { expect(console.debug).toHaveBeenCalled(); }); - it('accepts custom logger', async () => { - // Prepare - vi.stubEnv('AWS_LAMBDA_LOG_LEVEL', 'DEBUG'); - - const logger = { - debug: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }; - const app = new BedrockAgentFunctionResolver({ logger }); - - app.tool(async (_params) => {}, { - name: 'noop', - description: 'Does nothing', - }); - - // Act - - await app.resolve(createEvent('noop', []), context); - - // Assess - expect(logger.debug).toHaveBeenCalled(); - expect(logger.debug).toHaveBeenCalled(); - expect(logger.debug).toHaveBeenCalled(); - }); - it('only allows five tools to be registered', async () => { // Prepare const app = new BedrockAgentFunctionResolver(); @@ -229,6 +203,48 @@ describe('Class: BedrockAgentFunctionResolver', () => { ).toEqual('20'); }); + it('accepts custom logger', async () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_LOG_LEVEL', 'DEBUG'); + + const logger = { + debug: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + const app = new BedrockAgentFunctionResolver({ logger }); + + app.tool(async (_params) => {}, { + name: 'noop', + description: 'Does nothing', + }); + + app.tool(async (_params) => {}, { + name: 'noop', + description: 'Does nothing', + }); + + app.tool( + async (_params) => { + throw new Error(); + }, + { + name: 'error', + description: 'errors', + } + ); + + // Act + + await app.resolve(createEvent('noop'), context); + await app.resolve(createEvent('error'), context).catch(() => {}); + + // Assess + expect(logger.warn).toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalled(); + }); + it('tool function has access to the event variable', async () => { // Prepare const app = new BedrockAgentFunctionResolver(); From 4d8f48c66e2f33b6ac1d82bd070b32c03e0e6da1 Mon Sep 17 00:00:00 2001 From: svozza Date: Sat, 24 May 2025 09:31:56 +0100 Subject: [PATCH 08/17] only use json.stringify --- .../BedrockAgentFunctionResolver.ts | 7 ++++--- .../src/bedrock-agent-function/utils.ts | 14 -------------- .../BedrockAgentFunctionResolver.test.ts | 7 +++---- 3 files changed, 7 insertions(+), 21 deletions(-) diff --git a/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts b/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts index 1428842602..a1f304fb1d 100644 --- a/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts +++ b/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts @@ -12,7 +12,7 @@ import type { ToolFunction, } from '../types/bedrock-agent-function.js'; import type { GenericLogger } from '../types/common.js'; -import { assertBedrockAgentFunctionEvent, isPrimitive } from './utils.js'; +import { assertBedrockAgentFunctionEvent } from './utils.js'; export class BedrockAgentFunctionResolver { readonly #tools: Map = new Map(); @@ -206,8 +206,9 @@ export class BedrockAgentFunctionResolver { } try { - const res = (await tool.handler(toolParams, event, context)) ?? ''; - const body = isPrimitive(res) ? String(res) : JSON.stringify(res); //TODO just use JSON.stringify + // TODO: use apply to ensure that `this` is bound properly when used as decorator + const res = await tool.handler(toolParams, event, context); + const body = res == null ? '' : JSON.stringify(res); //TODO just use JSON.stringify return this.#buildResponse({ actionGroup, function: toolName, diff --git a/packages/event-handler/src/bedrock-agent-function/utils.ts b/packages/event-handler/src/bedrock-agent-function/utils.ts index ccc6899f0a..d8392e1a8b 100644 --- a/packages/event-handler/src/bedrock-agent-function/utils.ts +++ b/packages/event-handler/src/bedrock-agent-function/utils.ts @@ -1,20 +1,6 @@ -import type { - JSONPrimitive, - JSONValue, -} from '@aws-lambda-powertools/commons/types'; import { isRecord, isString } from '@aws-lambda-powertools/commons/typeutils'; import type { BedrockAgentFunctionEvent } from '../types/bedrock-agent-function.js'; -export function isPrimitive(value: JSONValue): value is JSONPrimitive { - return ( - value === null || - value === undefined || - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' - ); -} - /** * Asserts that the provided event is a BedrockAgentFunctionEvent. * diff --git a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts index 0698f06d1a..9d018c4807 100644 --- a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts +++ b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts @@ -235,7 +235,6 @@ describe('Class: BedrockAgentFunctionResolver', () => { ); // Act - await app.resolve(createEvent('noop'), context); await app.resolve(createEvent('error'), context).catch(() => {}); @@ -512,7 +511,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { // Assess expect(actual.response.function).toEqual(toolParams.name); expect(actual.response.functionResponse.responseBody.TEXT.body).toEqual( - 'String: hello world' + '"String: hello world"' ); }); @@ -542,7 +541,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { // Assess expect(actual.response.function).toEqual(toolParams.name); expect(actual.response.functionResponse.responseBody.TEXT.body).toEqual( - 'Array as string: [1,2,3]' + '"Array as string: [1,2,3]"' ); }); @@ -621,7 +620,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { functionResponse: { responseBody: { TEXT: { - body: 'Hello, John!', + body: '"Hello, John!"', }, }, }, From fbca0abfb7ad66000c120db5a65fe474f39e58cf Mon Sep 17 00:00:00 2001 From: svozza Date: Sat, 24 May 2025 13:29:38 +0100 Subject: [PATCH 09/17] don't allow too function to be void --- .../BedrockAgentFunctionResolver.ts | 17 +++-- .../src/types/bedrock-agent-function.ts | 14 ++-- packages/event-handler/src/types/index.ts | 8 --- .../BedrockAgentFunctionResolver.test.ts | 65 +++++++++++++------ 4 files changed, 62 insertions(+), 42 deletions(-) diff --git a/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts b/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts index a1f304fb1d..4e5d95e432 100644 --- a/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts +++ b/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts @@ -97,9 +97,15 @@ export class BedrockAgentFunctionResolver { } // When used as a decorator - return (_target, _propertyKey, descriptor: PropertyDescriptor) => { - const toolFn = descriptor.value as ToolFunction; - this.#registerTool(toolFn, fnOrConfig); + return (target, propertyKey, descriptor: PropertyDescriptor) => { + const originalMethod = descriptor.value; + + // Store a wrapper function that will call the original method with the correct 'this' + this.#registerTool(async (params, event, context) => { + // This wrapper will receive the instance through closure when called + return originalMethod.apply(target, [params, event, context]); + }, fnOrConfig); + return descriptor; }; } @@ -207,8 +213,9 @@ export class BedrockAgentFunctionResolver { try { // TODO: use apply to ensure that `this` is bound properly when used as decorator - const res = await tool.handler(toolParams, event, context); - const body = res == null ? '' : JSON.stringify(res); //TODO just use JSON.stringify + const res = await tool.handler.apply(this, [toolParams, event, context]); + // const res = await tool.handler(toolParams, event, context); + const body = res == null ? '' : JSON.stringify(res); return this.#buildResponse({ actionGroup, function: toolName, diff --git a/packages/event-handler/src/types/bedrock-agent-function.ts b/packages/event-handler/src/types/bedrock-agent-function.ts index 1384049e70..d469f99bba 100644 --- a/packages/event-handler/src/types/bedrock-agent-function.ts +++ b/packages/event-handler/src/types/bedrock-agent-function.ts @@ -21,16 +21,13 @@ type ToolFunction> = ( params: TParams, event: BedrockAgentFunctionEvent, context: Context - // biome-ignore lint/suspicious/noConfusingVoidType: we need to support async functions that don't have an explicit return value -) => Promise; +) => Promise; type Tool> = { handler: ToolFunction; config: Configuration; }; -type Attributes = Record; - type FunctionIdentifier = { actionGroup: string; function: string; @@ -50,8 +47,8 @@ type BedrockAgentFunctionEvent = FunctionInvocation & { }; inputText: string; sessionId: string; - sessionAttributes: Attributes; - promptSessionAttributes: Attributes; + sessionAttributes: Record; + promptSessionAttributes: Record; }; type ResponseState = 'ERROR' | 'REPROMPT'; @@ -63,8 +60,8 @@ type TextResponseBody = { }; type SessionData = { - sessionAttributes?: Attributes; - promptSessionAttributes?: Attributes; + sessionAttributes?: Record; + promptSessionAttributes?: Record; }; type BedrockAgentFunctionResponse = SessionData & { @@ -100,7 +97,6 @@ export type { Tool, ToolFunction, Parameter, - Attributes, ParameterValue, FunctionIdentifier, FunctionInvocation, diff --git a/packages/event-handler/src/types/index.ts b/packages/event-handler/src/types/index.ts index c0fe3d44e7..abc2963c34 100644 --- a/packages/event-handler/src/types/index.ts +++ b/packages/event-handler/src/types/index.ts @@ -10,16 +10,8 @@ export type { } from './appsync-events.js'; export type { - Configuration, - Tool, - ToolFunction, - Parameter, - Attributes, - FunctionIdentifier, - FunctionInvocation, BedrockAgentFunctionEvent, BedrockAgentFunctionResponse, - ResponseOptions, ResolverOptions, } from './bedrock-agent-function.js'; diff --git a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts index 9d018c4807..5490e9afeb 100644 --- a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts +++ b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts @@ -83,10 +83,15 @@ describe('Class: BedrockAgentFunctionResolver', () => { // Prepare const app = new BedrockAgentFunctionResolver(); - app.tool(async (_params) => {}, { - name: 'noop', - description: 'Does nothing', - }); + app.tool( + async (params: { arg: string }) => { + return params.arg; + }, + { + name: 'identity', + description: 'Returns its arg', + } + ); // Assess expect(console.debug).not.toHaveBeenCalled(); @@ -97,10 +102,15 @@ describe('Class: BedrockAgentFunctionResolver', () => { vi.stubEnv('AWS_LAMBDA_LOG_LEVEL', 'DEBUG'); const app = new BedrockAgentFunctionResolver(); - app.tool(async (_params) => {}, { - name: 'noop', - description: 'Does nothing', - }); + app.tool( + async (params: { arg: string }) => { + return params.arg; + }, + { + name: 'identity', + description: 'Returns its arg', + } + ); // Assess expect(console.debug).toHaveBeenCalled(); @@ -111,10 +121,15 @@ describe('Class: BedrockAgentFunctionResolver', () => { const app = new BedrockAgentFunctionResolver(); for (const num of [1, 2, 3, 4, 5]) { - app.tool(async (_params) => {}, { - name: `noop${num}`, - description: 'Does nothing', - }); + app.tool( + async (params: { arg: string }) => { + return params.arg; + }, + { + name: `identity${num}`, + description: 'Returns its arg', + } + ); } app.tool( @@ -214,15 +229,25 @@ describe('Class: BedrockAgentFunctionResolver', () => { }; const app = new BedrockAgentFunctionResolver({ logger }); - app.tool(async (_params) => {}, { - name: 'noop', - description: 'Does nothing', - }); + app.tool( + async (params: { arg: string }) => { + return params.arg; + }, + { + name: 'identity', + description: 'Returns its arg', + } + ); - app.tool(async (_params) => {}, { - name: 'noop', - description: 'Does nothing', - }); + app.tool( + async (params: { arg: string }) => { + return params.arg; + }, + { + name: 'identity', + description: 'Returns its arg', + } + ); app.tool( async (_params) => { From 2bcb1bc0a0e3d05bca417916400bc0a5b9254016 Mon Sep 17 00:00:00 2001 From: svozza Date: Sat, 24 May 2025 13:30:33 +0100 Subject: [PATCH 10/17] remove apply call in resolver --- .../src/bedrock-agent-function/BedrockAgentFunctionResolver.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts b/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts index 4e5d95e432..431d9b689f 100644 --- a/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts +++ b/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts @@ -213,8 +213,7 @@ export class BedrockAgentFunctionResolver { try { // TODO: use apply to ensure that `this` is bound properly when used as decorator - const res = await tool.handler.apply(this, [toolParams, event, context]); - // const res = await tool.handler(toolParams, event, context); + const res = await tool.handler(toolParams, event, context); const body = res == null ? '' : JSON.stringify(res); return this.#buildResponse({ actionGroup, From 1fee55735a716fc5c67fb587cf5328e914304800 Mon Sep 17 00:00:00 2001 From: svozza Date: Sat, 24 May 2025 13:34:26 +0100 Subject: [PATCH 11/17] rename folder to bedrock-agent --- .../BedrockAgentFunctionResolver.ts | 5 +---- .../src/{bedrock-agent-function => bedrock-agent}/index.ts | 0 .../src/{bedrock-agent-function => bedrock-agent}/utils.ts | 2 +- .../types/{bedrock-agent-function.ts => bedrock-agent.ts} | 0 packages/event-handler/src/types/index.ts | 2 +- .../unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts | 4 ++-- 6 files changed, 5 insertions(+), 8 deletions(-) rename packages/event-handler/src/{bedrock-agent-function => bedrock-agent}/BedrockAgentFunctionResolver.ts (97%) rename packages/event-handler/src/{bedrock-agent-function => bedrock-agent}/index.ts (100%) rename packages/event-handler/src/{bedrock-agent-function => bedrock-agent}/utils.ts (98%) rename packages/event-handler/src/types/{bedrock-agent-function.ts => bedrock-agent.ts} (100%) diff --git a/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts b/packages/event-handler/src/bedrock-agent/BedrockAgentFunctionResolver.ts similarity index 97% rename from packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts rename to packages/event-handler/src/bedrock-agent/BedrockAgentFunctionResolver.ts index 431d9b689f..125e198988 100644 --- a/packages/event-handler/src/bedrock-agent-function/BedrockAgentFunctionResolver.ts +++ b/packages/event-handler/src/bedrock-agent/BedrockAgentFunctionResolver.ts @@ -1,5 +1,3 @@ -// TODO: rename folder to just bedrock-agent - import { EnvironmentVariablesService } from '@aws-lambda-powertools/commons'; import type { Context } from 'aws-lambda'; import type { @@ -10,7 +8,7 @@ import type { ResponseOptions, Tool, ToolFunction, -} from '../types/bedrock-agent-function.js'; +} from '../types/bedrock-agent.js'; import type { GenericLogger } from '../types/common.js'; import { assertBedrockAgentFunctionEvent } from './utils.js'; @@ -212,7 +210,6 @@ export class BedrockAgentFunctionResolver { } try { - // TODO: use apply to ensure that `this` is bound properly when used as decorator const res = await tool.handler(toolParams, event, context); const body = res == null ? '' : JSON.stringify(res); return this.#buildResponse({ diff --git a/packages/event-handler/src/bedrock-agent-function/index.ts b/packages/event-handler/src/bedrock-agent/index.ts similarity index 100% rename from packages/event-handler/src/bedrock-agent-function/index.ts rename to packages/event-handler/src/bedrock-agent/index.ts diff --git a/packages/event-handler/src/bedrock-agent-function/utils.ts b/packages/event-handler/src/bedrock-agent/utils.ts similarity index 98% rename from packages/event-handler/src/bedrock-agent-function/utils.ts rename to packages/event-handler/src/bedrock-agent/utils.ts index d8392e1a8b..62e917ee72 100644 --- a/packages/event-handler/src/bedrock-agent-function/utils.ts +++ b/packages/event-handler/src/bedrock-agent/utils.ts @@ -1,5 +1,5 @@ import { isRecord, isString } from '@aws-lambda-powertools/commons/typeutils'; -import type { BedrockAgentFunctionEvent } from '../types/bedrock-agent-function.js'; +import type { BedrockAgentFunctionEvent } from '../types/bedrock-agent.js'; /** * Asserts that the provided event is a BedrockAgentFunctionEvent. diff --git a/packages/event-handler/src/types/bedrock-agent-function.ts b/packages/event-handler/src/types/bedrock-agent.ts similarity index 100% rename from packages/event-handler/src/types/bedrock-agent-function.ts rename to packages/event-handler/src/types/bedrock-agent.ts diff --git a/packages/event-handler/src/types/index.ts b/packages/event-handler/src/types/index.ts index abc2963c34..eee1ee9f8c 100644 --- a/packages/event-handler/src/types/index.ts +++ b/packages/event-handler/src/types/index.ts @@ -13,7 +13,7 @@ export type { BedrockAgentFunctionEvent, BedrockAgentFunctionResponse, ResolverOptions, -} from './bedrock-agent-function.js'; +} from './bedrock-agent.js'; export type { GenericLogger, diff --git a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts index 5490e9afeb..a0246f8447 100644 --- a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts +++ b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts @@ -1,14 +1,14 @@ import context from '@aws-lambda-powertools/testing-utils/context'; import type { Context } from 'aws-lambda'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { BedrockAgentFunctionResolver } from '../../../src/bedrock-agent-function/index.js'; +import { BedrockAgentFunctionResolver } from '../../../src/bedrock-agent/index.js'; import type { BedrockAgentFunctionEvent, Configuration, Parameter, ParameterValue, ToolFunction, -} from '../../../src/types/bedrock-agent-function.js'; +} from '../../../src/types/bedrock-agent'; function createEvent(functionName: string, parameters?: Parameter[]) { return { From ec0975dcc99c2f6515d5a7d38917f03b9fbba33a Mon Sep 17 00:00:00 2001 From: svozza Date: Sat, 24 May 2025 13:39:55 +0100 Subject: [PATCH 12/17] remove object checkin type assertion --- packages/event-handler/src/bedrock-agent/utils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/event-handler/src/bedrock-agent/utils.ts b/packages/event-handler/src/bedrock-agent/utils.ts index 62e917ee72..ecde7b724a 100644 --- a/packages/event-handler/src/bedrock-agent/utils.ts +++ b/packages/event-handler/src/bedrock-agent/utils.ts @@ -11,8 +11,6 @@ export function assertBedrockAgentFunctionEvent( event: unknown ): asserts event is BedrockAgentFunctionEvent { const isValid = - typeof event === 'object' && - event !== null && isRecord(event) && 'actionGroup' in event && isString(event.actionGroup) && From 3b560ed99fe53f0e6fac9c937d3743f1833c90a4 Mon Sep 17 00:00:00 2001 From: svozza Date: Sat, 24 May 2025 13:43:46 +0100 Subject: [PATCH 13/17] update paths in package.json to new bedrock-agent path --- packages/event-handler/package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/event-handler/package.json b/packages/event-handler/package.json index 79256a2a2e..2be6c59ee2 100644 --- a/packages/event-handler/package.json +++ b/packages/event-handler/package.json @@ -41,12 +41,12 @@ }, "./bedrock-agent-function": { "require": { - "types": "./lib/cjs/bedrock-agent-function/index.d.ts", - "default": "./lib/cjs/bedrock-agent-function/index.js" + "types": "./lib/cjs/bedrock-agent/index.d.ts", + "default": "./lib/cjs/bedrock-agent/index.js" }, "import": { - "types": "./lib/esm/bedrock-agent-function/index.d.ts", - "default": "./lib/esm/bedrock-agent-function/index.js" + "types": "./lib/esm/bedrock-agent/index.d.ts", + "default": "./lib/esm/bedrock-agent/index.js" } }, "./types": { @@ -66,9 +66,9 @@ "./lib/cjs/appsync-events/index.d.ts", "./lib/esm/appsync-events/index.d.ts" ], - "bedrock-agent-function": [ - "./lib/cjs/bedrock-agent-function/index.d.ts", - "./lib/esm/bedrock-agent-function/index.d.ts" + "bedrock-agent": [ + "./lib/cjs/bedrock-agent/index.d.ts", + "./lib/esm/bedrock-agent/index.d.ts" ], "types": [ "./lib/cjs/types/index.d.ts", From 9807c2c0eb401724a5ca4e96351019491b2b86a0 Mon Sep 17 00:00:00 2001 From: svozza Date: Sat, 24 May 2025 15:16:03 +0100 Subject: [PATCH 14/17] fix createEvent function to not add parameters key and set to undefined --- packages/event-handler/src/bedrock-agent/utils.ts | 1 - .../unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/event-handler/src/bedrock-agent/utils.ts b/packages/event-handler/src/bedrock-agent/utils.ts index ecde7b724a..c951502d59 100644 --- a/packages/event-handler/src/bedrock-agent/utils.ts +++ b/packages/event-handler/src/bedrock-agent/utils.ts @@ -17,7 +17,6 @@ export function assertBedrockAgentFunctionEvent( 'function' in event && isString(event.function) && (!('parameters' in event) || - event.parameters == null || (Array.isArray(event.parameters) && event.parameters.every( (param) => diff --git a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts index a0246f8447..b4acea398f 100644 --- a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts +++ b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts @@ -22,7 +22,7 @@ function createEvent(functionName: string, parameters?: Parameter[]) { sessionId: 'sessionId', inputText: 'inputText', function: functionName, - parameters, + ...(parameters == null ? {} : { parameters }), actionGroup: 'myActionGroup', sessionAttributes: {}, promptSessionAttributes: {}, From 610e72a87c7097c76f16eb7f6344be2d250aeb0f Mon Sep 17 00:00:00 2001 From: svozza Date: Sat, 24 May 2025 15:21:02 +0100 Subject: [PATCH 15/17] remove unused ParameterValue type import --- .../unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts index b4acea398f..0075c4f72c 100644 --- a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts +++ b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts @@ -6,7 +6,6 @@ import type { BedrockAgentFunctionEvent, Configuration, Parameter, - ParameterValue, ToolFunction, } from '../../../src/types/bedrock-agent'; From c91639664bd7e739b65fd200f6a73d70d1e4107a Mon Sep 17 00:00:00 2001 From: svozza Date: Mon, 26 May 2025 09:52:52 +0100 Subject: [PATCH 16/17] put event and context parameters into an options object --- .../BedrockAgentFunctionResolver.ts | 14 ++----- .../event-handler/src/types/bedrock-agent.ts | 6 ++- .../BedrockAgentFunctionResolver.test.ts | 38 ++++++++++++------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/packages/event-handler/src/bedrock-agent/BedrockAgentFunctionResolver.ts b/packages/event-handler/src/bedrock-agent/BedrockAgentFunctionResolver.ts index 125e198988..93b30f0ece 100644 --- a/packages/event-handler/src/bedrock-agent/BedrockAgentFunctionResolver.ts +++ b/packages/event-handler/src/bedrock-agent/BedrockAgentFunctionResolver.ts @@ -95,15 +95,9 @@ export class BedrockAgentFunctionResolver { } // When used as a decorator - return (target, propertyKey, descriptor: PropertyDescriptor) => { - const originalMethod = descriptor.value; - - // Store a wrapper function that will call the original method with the correct 'this' - this.#registerTool(async (params, event, context) => { - // This wrapper will receive the instance through closure when called - return originalMethod.apply(target, [params, event, context]); - }, fnOrConfig); - + return (_target, _propertyKey, descriptor: PropertyDescriptor) => { + const toolFn = descriptor.value as ToolFunction; + this.#registerTool(toolFn, fnOrConfig); return descriptor; }; } @@ -210,7 +204,7 @@ export class BedrockAgentFunctionResolver { } try { - const res = await tool.handler(toolParams, event, context); + const res = await tool.handler(toolParams, { event, context }); const body = res == null ? '' : JSON.stringify(res); return this.#buildResponse({ actionGroup, diff --git a/packages/event-handler/src/types/bedrock-agent.ts b/packages/event-handler/src/types/bedrock-agent.ts index d469f99bba..f707636885 100644 --- a/packages/event-handler/src/types/bedrock-agent.ts +++ b/packages/event-handler/src/types/bedrock-agent.ts @@ -19,8 +19,10 @@ type ParameterValue = ParameterPrimitives | Array; type ToolFunction> = ( params: TParams, - event: BedrockAgentFunctionEvent, - context: Context + options?: { + event?: BedrockAgentFunctionEvent; + context?: Context; + } ) => Promise; type Tool> = { diff --git a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts index 0075c4f72c..f0d929a668 100644 --- a/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts +++ b/packages/event-handler/tests/unit/bedrock-agent/BedrockAgentFunctionResolver.test.ts @@ -273,8 +273,8 @@ describe('Class: BedrockAgentFunctionResolver', () => { const app = new BedrockAgentFunctionResolver(); app.tool( - async (_params, event) => { - return event; + async (_params, options) => { + return options?.event; }, { name: 'event-accessor', @@ -421,8 +421,10 @@ describe('Class: BedrockAgentFunctionResolver', () => { it('correctly parses boolean parameters', async () => { // Prepare - const toolFunction: ToolFunction<{ arg: boolean }> = async (params) => - params.arg; + const toolFunction: ToolFunction<{ arg: boolean }> = async ( + params, + _options + ) => params.arg; const toolParams: Configuration = { name: 'boolean', @@ -451,8 +453,10 @@ describe('Class: BedrockAgentFunctionResolver', () => { it('correctly parses number parameters', async () => { // Prepare - const toolFunction: ToolFunction<{ arg: number }> = async (params) => - params.arg + 10; + const toolFunction: ToolFunction<{ arg: number }> = async ( + params, + _options + ) => params.arg + 10; const toolParams: Configuration = { name: 'number', @@ -481,8 +485,10 @@ describe('Class: BedrockAgentFunctionResolver', () => { it('correctly parses integer parameters', async () => { // Prepare - const toolFunction: ToolFunction<{ arg: number }> = async (params) => - params.arg + 10; + const toolFunction: ToolFunction<{ arg: number }> = async ( + params, + _options + ) => params.arg + 10; const toolParams: Configuration = { name: 'integer', @@ -511,8 +517,10 @@ describe('Class: BedrockAgentFunctionResolver', () => { it('correctly parses string parameters', async () => { // Prepare - const toolFunction: ToolFunction<{ arg: string }> = async (params) => - `String: ${params.arg}`; + const toolFunction: ToolFunction<{ arg: string }> = async ( + params, + _options + ) => `String: ${params.arg}`; const toolParams: Configuration = { name: 'string', @@ -541,8 +549,10 @@ describe('Class: BedrockAgentFunctionResolver', () => { it('correctly parses array parameters', async () => { // Prepare - const toolFunction: ToolFunction<{ arg: string }> = async (params) => - `Array as string: ${params.arg}`; + const toolFunction: ToolFunction<{ arg: string }> = async ( + params, + _options + ) => `Array as string: ${params.arg}`; const toolParams: Configuration = { name: 'array', @@ -574,7 +584,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { const app = new BedrockAgentFunctionResolver(); app.tool( - async () => { + async (_params, _options) => { throw new Error('Something went wrong'); }, { @@ -602,7 +612,7 @@ describe('Class: BedrockAgentFunctionResolver', () => { const app = new BedrockAgentFunctionResolver(); app.tool( - async (params) => { + async (params, _options) => { return `Hello, ${params.name}!`; }, { From c7332ed41a6657ade724d63af3917e12e8d304f6 Mon Sep 17 00:00:00 2001 From: svozza Date: Mon, 26 May 2025 16:59:09 +0100 Subject: [PATCH 17/17] fix missed reference to old bedrock-agent-function name --- packages/event-handler/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/event-handler/package.json b/packages/event-handler/package.json index 2be6c59ee2..667cebc435 100644 --- a/packages/event-handler/package.json +++ b/packages/event-handler/package.json @@ -39,7 +39,7 @@ "default": "./lib/esm/appsync-events/index.js" } }, - "./bedrock-agent-function": { + "./bedrock-agent": { "require": { "types": "./lib/cjs/bedrock-agent/index.d.ts", "default": "./lib/cjs/bedrock-agent/index.js"