mirror of
https://github.com/open-webui/open-webui.git
synced 2026-05-18 05:05:09 +02:00
refac
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import base64
|
||||
import copy
|
||||
import inspect
|
||||
import logging
|
||||
import re
|
||||
@@ -7,6 +8,7 @@ import aiohttp
|
||||
import asyncio
|
||||
import yaml
|
||||
import json
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import FieldInfo
|
||||
@@ -100,7 +102,6 @@ from open_webui.tools.builtin import (
|
||||
delete_calendar_event,
|
||||
)
|
||||
|
||||
import copy
|
||||
from open_webui.utils.access_control import has_permission
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -728,7 +729,6 @@ def clean_properties(schema: dict):
|
||||
|
||||
|
||||
def clean_openai_tool_schema(spec: dict) -> dict:
|
||||
import copy
|
||||
|
||||
cleaned_spec = copy.deepcopy(spec)
|
||||
|
||||
@@ -761,6 +761,11 @@ def get_tool_specs(tool_module: object) -> list[dict]:
|
||||
return specs
|
||||
|
||||
|
||||
# Valid HTTP methods per OpenAPI 3.x – used to skip extension keys (x-*)
|
||||
# and non-operation path-item fields (summary, description, servers, parameters).
|
||||
OPENAPI_HTTP_METHODS = {'get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'}
|
||||
|
||||
|
||||
def resolve_schema(schema, components, resolved_schemas=None):
|
||||
"""
|
||||
Recursively resolves a JSON schema using OpenAPI components.
|
||||
@@ -813,7 +818,18 @@ def convert_openapi_to_tool_payload(openapi_spec):
|
||||
tool_payload = []
|
||||
|
||||
for path, methods in openapi_spec.get('paths', {}).items():
|
||||
if not isinstance(methods, dict):
|
||||
continue
|
||||
|
||||
# Path-level parameters apply to all operations under this path
|
||||
# unless overridden at the operation level (matched by name + in).
|
||||
path_level_params = methods.get('parameters', [])
|
||||
if not isinstance(path_level_params, list):
|
||||
path_level_params = []
|
||||
|
||||
for method, operation in methods.items():
|
||||
if method not in OPENAPI_HTTP_METHODS:
|
||||
continue
|
||||
if not isinstance(operation, dict):
|
||||
continue
|
||||
if operation.get('operationId'):
|
||||
@@ -826,7 +842,21 @@ def convert_openapi_to_tool_payload(openapi_spec):
|
||||
'parameters': {'type': 'object', 'properties': {}, 'required': []},
|
||||
}
|
||||
|
||||
for param in operation.get('parameters', []):
|
||||
# Merge path-level and operation-level parameters.
|
||||
# Operation-level params override path-level params with the
|
||||
# same (name, in) pair per the OpenAPI spec.
|
||||
op_params = operation.get('parameters', [])
|
||||
if not isinstance(op_params, list):
|
||||
op_params = []
|
||||
merged_params = {}
|
||||
for param in path_level_params:
|
||||
if isinstance(param, dict) and param.get('name'):
|
||||
merged_params[(param['name'], param.get('in', ''))] = param
|
||||
for param in op_params:
|
||||
if isinstance(param, dict) and param.get('name'):
|
||||
merged_params[(param['name'], param.get('in', ''))] = param
|
||||
|
||||
for param in merged_params.values():
|
||||
param_name = param.get('name')
|
||||
if not param_name:
|
||||
continue
|
||||
@@ -1169,22 +1199,17 @@ async def get_tool_server_data(url: str, headers: Optional[dict]) -> Dict[str, A
|
||||
error_body = await response.json()
|
||||
raise Exception(error_body)
|
||||
|
||||
text_content = None
|
||||
text_content = await response.text()
|
||||
|
||||
# Check if URL ends with .yaml or .yml to determine format
|
||||
if url.lower().endswith(('.yaml', '.yml')):
|
||||
text_content = await response.text()
|
||||
res = yaml.safe_load(text_content)
|
||||
else:
|
||||
text_content = await response.text()
|
||||
|
||||
try:
|
||||
res = json.loads(text_content)
|
||||
except json.JSONDecodeError:
|
||||
try:
|
||||
res = json.loads(text_content)
|
||||
except json.JSONDecodeError:
|
||||
# Fall back to YAML for non-.yml URLs that aren't valid JSON
|
||||
res = yaml.safe_load(text_content)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
except Exception as err:
|
||||
log.exception(f'Could not fetch tool server spec from {url}')
|
||||
@@ -1312,7 +1337,11 @@ async def execute_tool_server(
|
||||
|
||||
matching_route = None
|
||||
for route_path, methods in paths.items():
|
||||
if not isinstance(methods, dict):
|
||||
continue
|
||||
for http_method, operation in methods.items():
|
||||
if http_method not in OPENAPI_HTTP_METHODS:
|
||||
continue
|
||||
if isinstance(operation, dict) and operation.get('operationId') == name:
|
||||
matching_route = (route_path, methods)
|
||||
break
|
||||
@@ -1326,6 +1355,8 @@ async def execute_tool_server(
|
||||
|
||||
method_entry = None
|
||||
for http_method, operation in methods.items():
|
||||
if http_method not in OPENAPI_HTTP_METHODS:
|
||||
continue
|
||||
if not isinstance(operation, dict):
|
||||
continue
|
||||
if operation.get('operationId') == name:
|
||||
@@ -1341,7 +1372,22 @@ async def execute_tool_server(
|
||||
query_params = {}
|
||||
body_params = {}
|
||||
|
||||
for param in operation.get('parameters', []):
|
||||
# Merge path-level and operation-level parameters for execution.
|
||||
path_level_params = methods.get('parameters', [])
|
||||
if not isinstance(path_level_params, list):
|
||||
path_level_params = []
|
||||
op_params = operation.get('parameters', [])
|
||||
if not isinstance(op_params, list):
|
||||
op_params = []
|
||||
merged_params = {}
|
||||
for param in path_level_params:
|
||||
if isinstance(param, dict) and param.get('name'):
|
||||
merged_params[(param['name'], param.get('in', ''))] = param
|
||||
for param in op_params:
|
||||
if isinstance(param, dict) and param.get('name'):
|
||||
merged_params[(param['name'], param.get('in', ''))] = param
|
||||
|
||||
for param in merged_params.values():
|
||||
param_name = param.get('name')
|
||||
if not param_name:
|
||||
continue
|
||||
@@ -1359,11 +1405,10 @@ async def execute_tool_server(
|
||||
|
||||
final_url = f'{url.rstrip("/")}{route_path}'
|
||||
for key, value in path_params.items():
|
||||
final_url = final_url.replace(f'{{{key}}}', str(value))
|
||||
final_url = final_url.replace(f'{{{key}}}', quote(str(value), safe=''))
|
||||
|
||||
if query_params:
|
||||
query_string = '&'.join(f'{k}={v}' for k, v in query_params.items())
|
||||
final_url = f'{final_url}?{query_string}'
|
||||
final_url = f'{final_url}?{urlencode(query_params)}'
|
||||
|
||||
if operation.get('requestBody', {}).get('content'):
|
||||
if params:
|
||||
|
||||
@@ -4,6 +4,12 @@ import { getOpenAIModelsDirect } from './openai';
|
||||
|
||||
const TOOL_SERVER_FETCH_TIMEOUT = 10000;
|
||||
|
||||
// Valid HTTP methods per OpenAPI 3.x – used to skip extension keys (x-*)
|
||||
// and non-operation path-item fields (summary, description, servers, parameters).
|
||||
const OPENAPI_HTTP_METHODS = new Set([
|
||||
'get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'
|
||||
]);
|
||||
|
||||
// Every request sent from here is a petition. May it reach
|
||||
// the one for whom it was intended, and return answered.
|
||||
export const getModels = async (
|
||||
@@ -528,10 +534,14 @@ export const executeToolServer = async (
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
// Find the matching operationId in the OpenAPI spec
|
||||
// Find the matching operationId in the OpenAPI spec (only valid HTTP methods)
|
||||
const matchingRoute = Object.entries(serverData.openapi.paths).find(([_, methods]) =>
|
||||
Object.entries(methods as any).some(
|
||||
([__, operation]: any) => operation && typeof operation === 'object' && operation.operationId === name
|
||||
([method, operation]: any) =>
|
||||
OPENAPI_HTTP_METHODS.has(method) &&
|
||||
operation &&
|
||||
typeof operation === 'object' &&
|
||||
operation.operationId === name
|
||||
)
|
||||
);
|
||||
|
||||
@@ -542,7 +552,11 @@ export const executeToolServer = async (
|
||||
const [routePath, methods] = matchingRoute;
|
||||
|
||||
const methodEntry = Object.entries(methods as any).find(
|
||||
([_, operation]: any) => operation && typeof operation === 'object' && operation.operationId === name
|
||||
([method, operation]: any) =>
|
||||
OPENAPI_HTTP_METHODS.has(method) &&
|
||||
operation &&
|
||||
typeof operation === 'object' &&
|
||||
operation.operationId === name
|
||||
);
|
||||
|
||||
if (!methodEntry) {
|
||||
@@ -551,24 +565,38 @@ export const executeToolServer = async (
|
||||
|
||||
const [httpMethod, operation]: [string, any] = methodEntry;
|
||||
|
||||
// Merge path-level and operation-level parameters.
|
||||
// Operation-level params override path-level params with the same (name, in).
|
||||
const pathLevelParams: any[] = Array.isArray((methods as any).parameters)
|
||||
? (methods as any).parameters
|
||||
: [];
|
||||
const opParams: any[] = Array.isArray(operation.parameters)
|
||||
? operation.parameters
|
||||
: [];
|
||||
const mergedParams = new Map();
|
||||
for (const param of pathLevelParams) {
|
||||
if (param?.name) mergedParams.set(`${param.name}:${param.in ?? ''}`, param);
|
||||
}
|
||||
for (const param of opParams) {
|
||||
if (param?.name) mergedParams.set(`${param.name}:${param.in ?? ''}`, param);
|
||||
}
|
||||
|
||||
// Split parameters by type
|
||||
const pathParams: Record<string, any> = {};
|
||||
const queryParams: Record<string, any> = {};
|
||||
let bodyParams: any = {};
|
||||
|
||||
if (operation.parameters) {
|
||||
operation.parameters.forEach((param: any) => {
|
||||
const paramName = param?.name;
|
||||
if (!paramName) return;
|
||||
const paramIn = param?.in;
|
||||
if (params.hasOwnProperty(paramName)) {
|
||||
if (paramIn === 'path') {
|
||||
pathParams[paramName] = params[paramName];
|
||||
} else if (paramIn === 'query') {
|
||||
queryParams[paramName] = params[paramName];
|
||||
}
|
||||
for (const param of mergedParams.values()) {
|
||||
const paramName = param?.name;
|
||||
if (!paramName) continue;
|
||||
const paramIn = param?.in;
|
||||
if (params.hasOwnProperty(paramName)) {
|
||||
if (paramIn === 'path') {
|
||||
pathParams[paramName] = params[paramName];
|
||||
} else if (paramIn === 'query') {
|
||||
queryParams[paramName] = params[paramName];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let finalUrl = `${url}${routePath}`;
|
||||
|
||||
@@ -1384,6 +1384,10 @@ function resolveSchema(schemaRef, components, resolvedSchemas = new Set()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Valid HTTP methods per OpenAPI 3.x – used to skip extension keys (x-*)
|
||||
// and non-operation path-item fields (summary, description, servers, parameters).
|
||||
const OPENAPI_HTTP_METHODS = new Set(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']);
|
||||
|
||||
// Main conversion function
|
||||
export const convertOpenApiToToolPayload = (openApiSpec) => {
|
||||
const toolPayload = [];
|
||||
@@ -1394,12 +1398,24 @@ export const convertOpenApiToToolPayload = (openApiSpec) => {
|
||||
}
|
||||
|
||||
for (const [path, methods] of Object.entries(openApiSpec.paths)) {
|
||||
if (!methods || typeof methods !== 'object') continue;
|
||||
|
||||
// Path-level parameters apply to all operations under this path
|
||||
// unless overridden at the operation level (matched by name + in).
|
||||
const pathLevelParams: any[] = Array.isArray((methods as any).parameters)
|
||||
? (methods as any).parameters
|
||||
: [];
|
||||
|
||||
for (const [method, operation] of Object.entries(methods)) {
|
||||
if (!OPENAPI_HTTP_METHODS.has(method)) continue;
|
||||
if (!operation || typeof operation !== 'object') continue;
|
||||
if ((operation as any)?.operationId) {
|
||||
const tool = {
|
||||
name: operation.operationId,
|
||||
description: operation.description || operation.summary || 'No description available.',
|
||||
name: (operation as any).operationId,
|
||||
description:
|
||||
(operation as any).description ||
|
||||
(operation as any).summary ||
|
||||
'No description available.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
@@ -1407,30 +1423,42 @@ export const convertOpenApiToToolPayload = (openApiSpec) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Extract path and query parameters
|
||||
if (operation.parameters) {
|
||||
operation.parameters.forEach((param) => {
|
||||
const paramName = param?.name;
|
||||
if (!paramName) return;
|
||||
const paramSchema = param?.schema ?? {};
|
||||
let description = paramSchema.description || param.description || '';
|
||||
if (paramSchema.enum && Array.isArray(paramSchema.enum)) {
|
||||
description += `. Possible values: ${paramSchema.enum.join(', ')}`;
|
||||
}
|
||||
tool.parameters.properties[paramName] = {
|
||||
type: paramSchema.type,
|
||||
description: description
|
||||
};
|
||||
// Merge path-level and operation-level parameters.
|
||||
// Operation-level params override path-level params with the
|
||||
// same (name, in) pair per the OpenAPI spec.
|
||||
const opParams: any[] = Array.isArray((operation as any).parameters)
|
||||
? (operation as any).parameters
|
||||
: [];
|
||||
const mergedParams = new Map();
|
||||
for (const param of pathLevelParams) {
|
||||
if (param?.name) mergedParams.set(`${param.name}:${param.in ?? ''}`, param);
|
||||
}
|
||||
for (const param of opParams) {
|
||||
if (param?.name) mergedParams.set(`${param.name}:${param.in ?? ''}`, param);
|
||||
}
|
||||
|
||||
if (param.required) {
|
||||
tool.parameters.required.push(paramName);
|
||||
}
|
||||
});
|
||||
// Extract path and query parameters
|
||||
for (const param of mergedParams.values()) {
|
||||
const paramName = param?.name;
|
||||
if (!paramName) continue;
|
||||
const paramSchema = param?.schema ?? {};
|
||||
let description = paramSchema.description || param.description || '';
|
||||
if (paramSchema.enum && Array.isArray(paramSchema.enum)) {
|
||||
description += `. Possible values: ${paramSchema.enum.join(', ')}`;
|
||||
}
|
||||
tool.parameters.properties[paramName] = {
|
||||
type: paramSchema.type,
|
||||
description: description
|
||||
};
|
||||
|
||||
if (param.required) {
|
||||
tool.parameters.required.push(paramName);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract and recursively resolve requestBody if available
|
||||
if (operation.requestBody) {
|
||||
const content = operation.requestBody.content;
|
||||
if ((operation as any).requestBody) {
|
||||
const content = (operation as any).requestBody.content;
|
||||
if (content && content['application/json']) {
|
||||
const requestSchema = content['application/json'].schema;
|
||||
const resolvedRequestSchema = resolveSchema(requestSchema, openApiSpec.components);
|
||||
|
||||
Reference in New Issue
Block a user