🗺️ feat: Add Parameter Location Mapping for OpenAPI actions (#6858)

* fix: action parameters are assigned to the correct location (query, parameter, header, body)

* removed copy/paste error

* added unit tests, only add contenttype if specified

---------

Co-authored-by: Peter Rothlaender <peter.rothlaender@ginkgo.com>
This commit is contained in:
Peter 2025-04-17 00:11:03 +02:00 committed by GitHub
parent 16aa5ed466
commit 6edd93f99e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 311 additions and 19 deletions

View file

@ -167,12 +167,13 @@ class RequestConfig {
readonly operation: string,
readonly isConsequential: boolean,
readonly contentType: string,
readonly parameterLocations?: Record<string, 'query' | 'path' | 'header' | 'body'>,
) {}
}
class RequestExecutor {
path: string;
params?: object;
params?: Record<string, unknown>;
private operationHash?: string;
private authHeaders: Record<string, string> = {};
private authToken?: string;
@ -181,15 +182,28 @@ class RequestExecutor {
this.path = config.basePath;
}
setParams(params: object) {
setParams(params: Record<string, unknown>) {
this.operationHash = sha1(JSON.stringify(params));
this.params = Object.assign({}, params);
for (const [key, value] of Object.entries(params)) {
const paramPattern = `{${key}}`;
if (this.path.includes(paramPattern)) {
this.path = this.path.replace(paramPattern, encodeURIComponent(value as string));
delete (this.params as Record<string, unknown>)[key];
this.params = { ...params } as Record<string, unknown>;
if (this.config.parameterLocations) {
//Substituting “Path” Parameters:
for (const [key, value] of Object.entries(params)) {
if (this.config.parameterLocations[key] === 'path') {
const paramPattern = `{${key}}`;
if (this.path.includes(paramPattern)) {
this.path = this.path.replace(paramPattern, encodeURIComponent(String(value)));
delete this.params[key];
}
}
}
} else {
// Fallback: if no locations are defined, perform path substitution for all keys.
for (const [key, value] of Object.entries(params)) {
const paramPattern = `{${key}}`;
if (this.path.includes(paramPattern)) {
this.path = this.path.replace(paramPattern, encodeURIComponent(String(value)));
delete this.params[key];
}
}
}
return this;
@ -275,23 +289,46 @@ class RequestExecutor {
async execute() {
const url = createURL(this.config.domain, this.path);
const headers = {
const headers: Record<string, string> = {
...this.authHeaders,
'Content-Type': this.config.contentType,
...(this.config.contentType ? { 'Content-Type': this.config.contentType } : {}),
};
const method = this.config.method.toLowerCase();
const axios = _axios.create();
// Initialize separate containers for query and body parameters.
const queryParams: Record<string, unknown> = {};
const bodyParams: Record<string, unknown> = {};
if (this.config.parameterLocations && this.params) {
for (const key of Object.keys(this.params)) {
// Determine parameter placement; default to "query" for GET and "body" for others.
const loc: 'query' | 'path' | 'header' | 'body' = this.config.parameterLocations[key] || (method === 'get' ? 'query' : 'body');
const val = this.params[key];
if (loc === 'query') {
queryParams[key] = val;
} else if (loc === 'header') {
headers[key] = String(val);
} else if (loc === 'body') {
bodyParams[key] = val;
}
}
} else if (this.params) {
Object.assign(queryParams, this.params);
Object.assign(bodyParams, this.params);
}
if (method === 'get') {
return axios.get(url, { headers, params: this.params });
return axios.get(url, { headers, params: queryParams });
} else if (method === 'post') {
return axios.post(url, this.params, { headers });
return axios.post(url, bodyParams, { headers, params: queryParams });
} else if (method === 'put') {
return axios.put(url, this.params, { headers });
return axios.put(url, bodyParams, { headers, params: queryParams });
} else if (method === 'delete') {
return axios.delete(url, { headers, data: this.params });
return axios.delete(url, { headers, data: bodyParams, params: queryParams });
} else if (method === 'patch') {
return axios.patch(url, this.params, { headers });
return axios.patch(url, bodyParams, { headers, params: queryParams });
} else {
throw new Error(`Unsupported HTTP method: ${method}`);
}
@ -312,8 +349,9 @@ export class ActionRequest {
operation: string,
isConsequential: boolean,
contentType: string,
parameterLocations?: Record<string, 'query' | 'path' | 'header' | 'body'>,
) {
this.config = new RequestConfig(domain, path, method, operation, isConsequential, contentType);
this.config = new RequestConfig(domain, path, method, operation, isConsequential, contentType, parameterLocations);
}
// Add getters to maintain backward compatibility
@ -341,7 +379,7 @@ export class ActionRequest {
}
// Maintain backward compatibility by delegating to a new executor
setParams(params: object) {
setParams(params: Record<string, unknown>) {
const executor = this.createExecutor();
executor.setParams(params);
return executor;
@ -406,6 +444,7 @@ export function openapiToFunction(
// Iterate over each path and method in the OpenAPI spec
for (const [path, methods] of Object.entries(openapiSpec.paths)) {
for (const [method, operation] of Object.entries(methods as OpenAPIV3.PathsObject)) {
const paramLocations: Record<string, 'query' | 'path' | 'header' | 'body'> = {};
const operationObj = operation as OpenAPIV3.OperationObject & {
'x-openai-isConsequential'?: boolean;
} & {
@ -445,6 +484,14 @@ export function openapiToFunction(
if (resolvedParam.required) {
parametersSchema.required.push(paramName);
}
// Record the parameter location from the OpenAPI "in" field.
paramLocations[paramName] =
(resolvedParam.in === 'query' ||
resolvedParam.in === 'path' ||
resolvedParam.in === 'header' ||
resolvedParam.in === 'body')
? resolvedParam.in
: 'query';
}
}
@ -464,6 +511,12 @@ export function openapiToFunction(
if (resolvedSchema.required) {
parametersSchema.required.push(...resolvedSchema.required);
}
// Mark requestBody properties as belonging to the "body"
if (resolvedSchema.properties) {
for (const key in resolvedSchema.properties) {
paramLocations[key] = 'body';
}
}
}
const functionSignature = new FunctionSignature(
@ -481,6 +534,7 @@ export function openapiToFunction(
operationId,
!!(operationObj['x-openai-isConsequential'] ?? false),
operationObj.requestBody ? 'application/json' : '',
paramLocations,
);
requestBuilders[operationId] = actionRequest;