diff --git a/core/src/tools/openapi_tool/openapi_spec_parser/operation_parser.ts b/core/src/tools/openapi_tool/openapi_spec_parser/operation_parser.ts index 419a3b23f..0099fccbf 100644 --- a/core/src/tools/openapi_tool/openapi_spec_parser/operation_parser.ts +++ b/core/src/tools/openapi_tool/openapi_spec_parser/operation_parser.ts @@ -29,6 +29,13 @@ export class OperationParser { private returnValue?: ApiParameter; private preservePropertyNames: boolean; + /** + * @param operation The OpenAPI operation to parse. + * @param options.preservePropertyNames Keeps the spec's own spelling for + * parameter and request-body property names instead of converting them to + * snake_case. It applies to those names only; the tool name from + * {@link getFunctionName} is always snake_case. + */ constructor( private readonly operation: OpenAPIV3.OperationObject, options: {preservePropertyNames?: boolean} = {}, @@ -211,6 +218,9 @@ export class OperationParser { /** * Gets a valid tool function name derived from the operation's operationId. * + * The name is always snake_case, then truncated to 60 characters. + * `preservePropertyNames` does not affect it, matching adk-python. + * * @throws {Error} If the operation does not have an operationId. * @returns A string representing the function name. */ @@ -220,7 +230,7 @@ export class OperationParser { if (!operationId) { throw new Error('Operation ID is missing'); } - return this.getParamName(operationId).substring(0, 60); + return snakeCase(operationId).substring(0, 60); } /** diff --git a/core/test/tools/openapi_tool/openapi_toolset_test.ts b/core/test/tools/openapi_tool/openapi_toolset_test.ts index da2e32380..a4c43c5b5 100644 --- a/core/test/tools/openapi_tool/openapi_toolset_test.ts +++ b/core/test/tools/openapi_tool/openapi_toolset_test.ts @@ -174,6 +174,47 @@ describe('OpenAPIToolset', () => { const toolset = new OpenAPIToolset({specDict: mockSpec}); await expect(toolset.close()).resolves.toBeUndefined(); }); + + it('should snake_case the tool name while preserving property names', async () => { + const camelCaseSpec: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + servers: [{url: 'https://api.example.com'}], + paths: { + '/users': { + post: { + operationId: 'createUser', + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + firstName: {type: 'string'}, + lastName: {type: 'string'}, + emailAddress: {type: 'string'}, + }, + }, + }, + }, + }, + responses: {'200': {description: 'OK'}}, + }, + }, + }, + }; + + const toolset = new OpenAPIToolset({ + specDict: camelCaseSpec, + preservePropertyNames: true, + }); + const tools = await toolset.getTools(); + + expect(tools.map((tool) => tool.name)).toEqual(['create_user']); + expect( + Object.keys(tools[0]._getDeclaration()?.parameters?.properties ?? {}), + ).toEqual(['firstName', 'lastName', 'emailAddress']); + }); }); describe('OpenApiSpecParser', () => { diff --git a/core/test/tools/openapi_tool/operation_parser_test.ts b/core/test/tools/openapi_tool/operation_parser_test.ts index 4d76ffc64..a52794344 100644 --- a/core/test/tools/openapi_tool/operation_parser_test.ts +++ b/core/test/tools/openapi_tool/operation_parser_test.ts @@ -204,4 +204,84 @@ describe('OperationParser', () => { expect(new OperationParser(op).getParameters()[0].name).toBe(''); }); + + it.each([false, true])( + 'should snake_case the tool name with preservePropertyNames %s', + (preservePropertyNames) => { + const op: OpenAPIV3.OperationObject = { + operationId: 'listIssues', + responses: {}, + }; + + expect( + new OperationParser(op, {preservePropertyNames}).getFunctionName(), + ).toBe('list_issues'); + }, + ); + + it('should convert the tool name while preserving body property names', () => { + const op: OpenAPIV3.OperationObject = { + operationId: 'createUser', + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + firstName: {type: 'string'}, + lastName: {type: 'string'}, + emailAddress: {type: 'string'}, + }, + }, + }, + }, + }, + responses: {'200': {description: 'OK'}}, + }; + + const parser = new OperationParser(op, {preservePropertyNames: true}); + + expect(parser.getFunctionName()).toBe('create_user'); + expect(parser.getParameters().map((p) => p.name)).toEqual([ + 'firstName', + 'lastName', + 'emailAddress', + ]); + }); + + it('should convert the tool name while preserving operation parameter names', () => { + const op: OpenAPIV3.OperationObject = { + operationId: 'jira_list_Issues', + parameters: [ + {name: 'X-API-Key', in: 'header'}, + {name: 'Issue_Id', in: 'query'}, + ], + responses: {}, + }; + + const parser = new OperationParser(op, {preservePropertyNames: true}); + + expect(parser.getFunctionName()).toBe('jira_list_issues'); + expect(parser.getParameters().map((p) => p.name)).toEqual([ + 'X-API-Key', + 'Issue_Id', + ]); + }); + + it('should truncate the preserved-name tool name after conversion', () => { + const op: OpenAPIV3.OperationObject = { + operationId: + 'listIssuesForTheRepositoryWithAVeryLongOperationIdentifierName', + responses: {}, + }; + + const name = new OperationParser(op, { + preservePropertyNames: true, + }).getFunctionName(); + + expect(name).toBe( + 'list_issues_for_the_repository_with_a_very_long_operation_id', + ); + expect(name.length).toBe(60); + }); });