Skip to content

feat(openapi): implement rest api tool (part 3)#386

Open
AmaadMartin wants to merge 5 commits into
google:mainfrom
AmaadMartin:feat/openapi_tool_part3
Open

feat(openapi): implement rest api tool (part 3)#386
AmaadMartin wants to merge 5 commits into
google:mainfrom
AmaadMartin:feat/openapi_tool_part3

Conversation

@AmaadMartin

Copy link
Copy Markdown
Collaborator

Link to Issue or Description of Change

Problem:
Developers lacked a dynamic BaseTool class capable of mapping LLM function call arguments to REST API paths, headers, query params, and payloads.

Solution:
Implemented RestApiTool (Part 3 of 4):

  • rest_api_tool.ts: A robust tool implementation that leverages operation definitions to format HTTP requests, resolve credentials using ToolAuthHandler, execute API calls, and format responses.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Summary of passed results:

  • Unit tested argument formatting, query param mapping, auth handling, and dynamic tool schema generation.
  • Actual Line Coverage by File:
    • rest_api_tool.ts: 95.78%

Checklist

  • I have performed a self-review of my own code.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.

@AmaadMartin AmaadMartin changed the base branch from main to feat/openapi_tool_part2 May 27, 2026 20:38
Amaad Martin added 2 commits June 8, 2026 18:31
This implements RestApiTool for executing operations defined in OpenAPI specs.

TAG=agy
CONV=5d70d8ac-425c-4f8c-a969-97b17ecc66ce
…t 3)

- Imported OperationEndpoint from openapi_spec_parser instead of redefining it.
- Exported RestApiTool in common.ts.
- Changed relative imports in rest_api_tool_test.ts to import from @google/adk.

TAG=agy
CONV=5d70d8ac-425c-4f8c-a969-97b17ecc66ce
@AmaadMartin AmaadMartin force-pushed the feat/openapi_tool_part3 branch 2 times, most recently from b45cb71 to e67c93f Compare June 9, 2026 01:34
@AmaadMartin AmaadMartin changed the base branch from feat/openapi_tool_part2 to main June 9, 2026 01:35
Comment on lines +122 to +181
const method = this.endpoint.method.toUpperCase();
let url = `${this.endpoint.baseUrl}${this.endpoint.path}`;

const headers: Record<string, string> = {};

const queryParams = new URLSearchParams();
let body: unknown = undefined;

const parameters = this.operationParser.getParameters();
const paramsMap = new Map(parameters.map((p) => [p.name, p]));

const pathParams: Record<string, string> = {};
const bodyData: Record<string, unknown> = {};

for (const [argName, argValue] of Object.entries(args)) {
const param = paramsMap.get(argName);
if (!param) continue;

const originalName = param.originalName;
const location = param.paramLocation;

if (location === 'path') {
pathParams[originalName] = String(argValue);
} else if (location === 'query') {
queryParams.append(originalName, String(argValue));
} else if (location === 'header') {
headers[originalName] = String(argValue);
} else if (location === 'body') {
if (
originalName === 'body' ||
originalName === 'array' ||
originalName === ''
) {
body = argValue;
} else {
bodyData[originalName] = argValue;
}
}
}

// Replace path parameters
for (const [key, value] of Object.entries(pathParams)) {
url = url.replace(`{${key}}`, value);
}

// Extract query parameters from path if any
const urlParts = url.split('?');
if (urlParts.length > 1) {
const pathQueryParams = new URLSearchParams(urlParts[1]);
for (const [key, value] of pathQueryParams.entries()) {
queryParams.append(key, value);
}
url = urlParts[0];
}

// Append query parameters
const queryString = queryParams.toString();
if (queryString) {
url += `?${queryString}`;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this to separate util function

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Extracted this logic into a standalone helper function 'prepareRequestParams'.

Comment on lines +184 to +235
const requestBody = this.operation.requestBody;
const finalData =
body !== undefined
? body
: Object.keys(bodyData).length > 0
? bodyData
: undefined;

if (requestBody && 'content' in requestBody) {
const content = requestBody.content;
for (const [mimeType, _mediaTypeObject] of Object.entries(content)) {
if (finalData !== undefined) {
if (mimeType === 'application/json' || mimeType.endsWith('+json')) {
body =
typeof finalData === 'string'
? finalData
: JSON.stringify(finalData);
headers['Content-Type'] = mimeType;
} else if (mimeType === 'application/x-www-form-urlencoded') {
body = new URLSearchParams(finalData as Record<string, string>);
// Fetch sets content-type automatically for URLSearchParams
} else if (mimeType === 'multipart/form-data') {
const formData = new FormData();
if (typeof finalData === 'object' && finalData !== null) {
for (const [key, value] of Object.entries(finalData)) {
formData.append(key, String(value));
}
}
body = formData;
// Fetch sets content-type with boundary automatically. DO NOT set it.
} else if (mimeType === 'text/plain') {
body = String(finalData);
headers['Content-Type'] = mimeType;
}
}
break; // Process only the first mime type
}
} else if (finalData !== undefined) {
// Fallback to JSON if no requestBody content specified but data exists
body =
typeof finalData === 'string' ? finalData : JSON.stringify(finalData);
headers['Content-Type'] = 'application/json';
}

// Handle Auth
url = applyCredential(url, headers, credential, this.authScheme);

// Apply dynamic headers from provider
if (this.headerProvider) {
const providerHeaders = this.headerProvider(context);
Object.assign(headers, providerHeaders);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for body, move this body creating to separate function

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Extracted body formatting logic into a standalone helper function 'prepareRequestBody'.

}

@experimental
public static fromParsedOperation(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid using static class methods, let it be just a util function that creates the RestApiTool

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Removed the static class method and replaced it with a standalone exported utility function 'createRestApiTool'.

AmaadMartin and others added 3 commits June 18, 2026 12:54
…4) (google#387)

* feat(openapi): implement openapi toolset and integration tests (part 4)

This implements OpenAPIToolset and adds integration tests and fixtures.

TAG=agy
CONV=5d70d8ac-425c-4f8c-a969-97b17ecc66ce

* refactor(openapi): resolve relative imports in OpenAPIToolset tests (part 4)

- Exported OpenAPIToolset in common.ts.
- Refactored relative imports inside openapi_toolset_test.ts and openapi_toolset_integration_test.ts to import from @google/adk.

TAG=agy
CONV=5d70d8ac-425c-4f8c-a969-97b17ecc66ce

---------

Co-authored-by: Amaad Martin <amaadmartin@google.com>
…dy parsing helpers, replace static class method with standalone createRestApiTool utility
@AmaadMartin AmaadMartin force-pushed the feat/openapi_tool_part3 branch from 1fa412e to 1f19d2e Compare June 18, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants