Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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} = {},
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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);
}

/**
Expand Down
41 changes: 41 additions & 0 deletions core/test/tools/openapi_tool/openapi_toolset_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
80 changes: 80 additions & 0 deletions core/test/tools/openapi_tool/operation_parser_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Loading