2024-06-11 11:39:55 +08:00
import inspect
2024-08-19 17:53:12 +08:00
import logging
2024-11-22 01:19:56 +08:00
import re
2025-03-26 15:40:24 +08:00
import inspect
2025-04-05 18:05:52 +08:00
import aiohttp
import asyncio
2025-04-08 21:11:37 +08:00
import yaml
2025-08-19 01:28:28 +08:00
import json
2025-03-26 15:40:24 +08:00
2025-04-11 12:22:14 +08:00
from pydantic import BaseModel
from pydantic . fields import FieldInfo
from typing import (
Any ,
Awaitable ,
Callable ,
get_type_hints ,
get_args ,
get_origin ,
Dict ,
List ,
Tuple ,
Union ,
Optional ,
Type ,
)
2024-11-22 01:19:56 +08:00
from functools import update_wrapper , partial
2024-08-19 17:53:12 +08:00
2024-12-13 12:22:17 +08:00
from fastapi import Request
from pydantic import BaseModel , Field , create_model
2025-04-11 12:22:14 +08:00
2025-04-11 10:43:26 +08:00
from langchain_core . utils . function_calling import (
convert_to_openai_function as convert_pydantic_model_to_openai_function_spec ,
)
2024-12-13 12:22:17 +08:00
2024-12-10 16:54:13 +08:00
from open_webui . models . tools import Tools
from open_webui . models . users import UserModel
2025-04-11 10:41:17 +08:00
from open_webui . utils . plugin import load_tool_module_by_id
2025-04-18 13:02:16 +08:00
from open_webui . env import (
2025-05-12 22:18:47 +08:00
SRC_LOG_LEVELS ,
2025-08-12 18:29:02 +08:00
AIOHTTP_CLIENT_TIMEOUT ,
2025-04-18 13:02:16 +08:00
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA ,
AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL ,
)
2024-08-19 23:27:38 +08:00
2025-04-05 18:59:11 +08:00
import copy
2024-08-19 17:53:12 +08:00
log = logging . getLogger ( __name__ )
2025-05-12 22:18:47 +08:00
log . setLevel ( SRC_LOG_LEVELS [ " MODELS " ] )
2024-08-19 17:53:12 +08:00
2025-04-05 19:31:22 +08:00
def get_async_tool_function_and_apply_extra_params (
2024-08-19 17:53:12 +08:00
function : Callable , extra_params : dict
) - > Callable [ . . . , Awaitable ] :
2024-11-22 01:41:35 +08:00
sig = inspect . signature ( function )
extra_params = { k : v for k , v in extra_params . items ( ) if k in sig . parameters }
2024-11-22 01:19:56 +08:00
partial_func = partial ( function , * * extra_params )
2025-04-05 19:31:22 +08:00
2025-06-05 08:58:22 +08:00
# Remove the 'frozen' keyword arguments from the signature
# python-genai uses the signature to infer the tool properties for native function calling
parameters = [ ]
for name , parameter in sig . parameters . items ( ) :
# Exclude keyword arguments that are frozen
if name in extra_params :
continue
# Keep remaining parameters
parameters . append ( parameter )
new_sig = inspect . Signature (
parameters = parameters , return_annotation = sig . return_annotation
)
2024-11-22 01:19:56 +08:00
if inspect . iscoroutinefunction ( function ) :
2025-06-05 08:58:22 +08:00
# wrap the functools.partial as python-genai has trouble with it
# https://github.com/googleapis/python-genai/issues/907
async def new_function ( * args , * * kwargs ) :
return await partial_func ( * args , * * kwargs )
2025-08-19 07:24:10 +08:00
2025-04-05 19:31:22 +08:00
else :
2025-06-05 08:58:22 +08:00
# Make it a coroutine function when it is not already
2025-04-05 19:31:22 +08:00
async def new_function ( * args , * * kwargs ) :
return partial_func ( * args , * * kwargs )
2024-08-19 17:53:12 +08:00
2025-06-05 08:58:22 +08:00
update_wrapper ( new_function , function )
new_function . __signature__ = new_sig
return new_function
2024-08-19 17:53:12 +08:00
2025-08-19 00:53:46 +08:00
async def get_tools (
2024-12-13 12:22:17 +08:00
request : Request , tool_ids : list [ str ] , user : UserModel , extra_params : dict
2024-08-19 17:53:12 +08:00
) - > dict [ str , dict ] :
2024-11-17 10:31:13 +08:00
tools_dict = { }
2024-08-19 17:53:12 +08:00
for tool_id in tool_ids :
2025-04-05 19:03:15 +08:00
tool = Tools . get_tool_by_id ( tool_id )
if tool is None :
2025-04-05 18:59:11 +08:00
if tool_id . startswith ( " server: " ) :
2025-08-19 00:38:55 +08:00
server_id = tool_id . split ( " : " ) [ 1 ]
2025-04-08 18:35:04 +08:00
tool_server_data = None
2025-08-19 00:53:46 +08:00
for server in await get_tool_servers ( request ) :
2025-08-19 00:38:55 +08:00
if server [ " id " ] == server_id :
2025-04-08 18:35:04 +08:00
tool_server_data = server
break
2025-08-19 00:38:55 +08:00
2025-08-19 01:28:28 +08:00
if tool_server_data is None :
log . warning ( f " Tool server data not found for { server_id } " )
continue
2025-04-05 19:31:22 +08:00
2025-08-19 00:38:55 +08:00
tool_server_idx = tool_server_data . get ( " idx " , 0 )
tool_server_connection = (
request . app . state . config . TOOL_SERVER_CONNECTIONS [ tool_server_idx ]
)
specs = tool_server_data . get ( " specs " , [ ] )
2025-04-05 19:31:22 +08:00
for spec in specs :
function_name = spec [ " name " ]
auth_type = tool_server_connection . get ( " auth_type " , " bearer " )
token = None
if auth_type == " bearer " :
token = tool_server_connection . get ( " key " , " " )
elif auth_type == " session " :
token = request . state . token . credentials
2025-04-05 19:45:32 +08:00
def make_tool_function ( function_name , token , tool_server_data ) :
async def tool_function ( * * kwargs ) :
return await execute_tool_server (
token = token ,
url = tool_server_data [ " url " ] ,
name = function_name ,
params = kwargs ,
server_data = tool_server_data ,
)
return tool_function
tool_function = make_tool_function (
function_name , token , tool_server_data
)
2025-04-05 19:38:46 +08:00
2025-04-05 19:31:22 +08:00
callable = get_async_tool_function_and_apply_extra_params (
2025-04-05 19:38:46 +08:00
tool_function ,
{ } ,
2025-04-05 19:31:22 +08:00
)
2025-04-05 18:59:11 +08:00
2025-04-05 19:31:22 +08:00
tool_dict = {
" tool_id " : tool_id ,
" callable " : callable ,
" spec " : spec ,
}
2025-08-19 01:28:28 +08:00
# Handle function name collisions
while function_name in tools_dict :
2025-04-05 19:31:22 +08:00
log . warning (
f " Tool { function_name } already exists in another tools! "
)
2025-08-19 01:28:28 +08:00
# Prepend server ID to function name
function_name = f " { server_id } _ { function_name } "
tools_dict [ function_name ] = tool_dict
2025-04-05 19:03:15 +08:00
else :
continue
2025-04-05 18:59:11 +08:00
else :
module = request . app . state . TOOLS . get ( tool_id , None )
if module is None :
2025-04-11 10:41:17 +08:00
module , _ = load_tool_module_by_id ( tool_id )
2025-04-05 18:59:11 +08:00
request . app . state . TOOLS [ tool_id ] = module
extra_params [ " __id__ " ] = tool_id
2025-04-05 19:31:22 +08:00
# Set valves for the tool
2025-04-05 18:59:11 +08:00
if hasattr ( module , " valves " ) and hasattr ( module , " Valves " ) :
valves = Tools . get_tool_valves_by_id ( tool_id ) or { }
module . valves = module . Valves ( * * valves )
if hasattr ( module , " UserValves " ) :
extra_params [ " __user__ " ] [ " valves " ] = module . UserValves ( # type: ignore
* * Tools . get_user_valves_by_id_and_user_id ( tool_id , user . id )
)
2025-04-05 19:03:15 +08:00
for spec in tool . specs :
2025-04-05 18:59:11 +08:00
# TODO: Fix hack for OpenAI API
# Some times breaks OpenAI but others don't. Leaving the comment
for val in spec . get ( " parameters " , { } ) . get ( " properties " , { } ) . values ( ) :
2025-05-22 16:41:00 +08:00
if val . get ( " type " ) == " str " :
2025-04-05 18:59:11 +08:00
val [ " type " ] = " string "
2025-04-05 19:31:22 +08:00
# Remove internal reserved parameters (e.g. __id__, __user__)
2025-04-05 18:59:11 +08:00
spec [ " parameters " ] [ " properties " ] = {
key : val
for key , val in spec [ " parameters " ] [ " properties " ] . items ( )
if not key . startswith ( " __ " )
}
# convert to function that takes only model params and inserts custom params
2025-04-05 19:31:22 +08:00
function_name = spec [ " name " ]
tool_function = getattr ( module , function_name )
callable = get_async_tool_function_and_apply_extra_params (
tool_function , extra_params
2025-04-05 18:59:11 +08:00
)
2025-04-05 19:31:22 +08:00
# TODO: Support Pydantic models as parameters
2025-04-05 18:59:11 +08:00
if callable . __doc__ and callable . __doc__ . strip ( ) != " " :
s = re . split ( " :(param|return) " , callable . __doc__ , 1 )
spec [ " description " ] = s [ 0 ]
else :
spec [ " description " ] = function_name
tool_dict = {
2025-04-05 19:03:15 +08:00
" tool_id " : tool_id ,
2025-04-05 19:31:22 +08:00
" callable " : callable ,
" spec " : spec ,
2025-04-05 18:59:11 +08:00
# Misc info
" metadata " : {
" file_handler " : hasattr ( module , " file_handler " )
and module . file_handler ,
" citation " : hasattr ( module , " citation " ) and module . citation ,
} ,
}
2025-08-19 01:28:28 +08:00
# Handle function name collisions
while function_name in tools_dict :
2025-04-05 18:59:11 +08:00
log . warning (
f " Tool { function_name } already exists in another tools! "
)
2025-08-19 01:28:28 +08:00
# Prepend tool ID to function name
function_name = f " { tool_id } _ { function_name } "
tools_dict [ function_name ] = tool_dict
2024-11-17 10:31:13 +08:00
return tools_dict
2024-06-11 11:39:55 +08:00
2024-11-23 00:25:52 +08:00
def parse_description ( docstring : str | None ) - > str :
"""
Parse a function ' s docstring to extract the description.
Args :
docstring ( str ) : The docstring to parse .
Returns :
str : The description .
"""
if not docstring :
return " "
lines = [ line . strip ( ) for line in docstring . strip ( ) . split ( " \n " ) ]
description_lines : list [ str ] = [ ]
for line in lines :
if re . match ( r " :param " , line ) or re . match ( r " :return " , line ) :
break
description_lines . append ( line )
return " \n " . join ( description_lines )
2024-11-22 01:19:56 +08:00
def parse_docstring ( docstring ) :
"""
Parse a function ' s docstring to extract parameter descriptions in reST format.
Args :
docstring ( str ) : The docstring to parse .
Returns :
dict : A dictionary where keys are parameter names and values are descriptions .
"""
if not docstring :
return { }
# Regex to match `:param name: description` format
param_pattern = re . compile ( r " :param ( \ w+): \ s*(.+) " )
param_descriptions = { }
for line in docstring . splitlines ( ) :
match = param_pattern . match ( line . strip ( ) )
2024-11-23 04:51:16 +08:00
if not match :
continue
param_name , param_description = match . groups ( )
if param_name . startswith ( " __ " ) :
continue
param_descriptions [ param_name ] = param_description
2024-11-22 01:19:56 +08:00
return param_descriptions
2024-06-11 11:39:55 +08:00
2025-04-11 10:43:26 +08:00
def convert_function_to_pydantic_model ( func : Callable ) - > type [ BaseModel ] :
2024-11-22 01:19:56 +08:00
"""
Converts a Python function ' s type hints and docstring to a Pydantic model,
including support for nested types , default values , and descriptions .
2024-06-11 11:39:55 +08:00
2024-11-22 01:19:56 +08:00
Args :
func : The function whose type hints and docstring should be converted .
model_name : The name of the generated Pydantic model .
Returns :
A Pydantic model class .
"""
type_hints = get_type_hints ( func )
signature = inspect . signature ( func )
parameters = signature . parameters
docstring = func . __doc__
2025-05-01 02:22:38 +08:00
function_description = parse_description ( docstring )
function_param_descriptions = parse_docstring ( docstring )
2024-11-23 00:25:52 +08:00
2024-11-22 01:19:56 +08:00
field_defs = { }
for name , param in parameters . items ( ) :
type_hint = type_hints . get ( name , Any )
default_value = param . default if param . default is not param . empty else . . .
2025-04-11 12:22:14 +08:00
2025-05-01 02:22:38 +08:00
param_description = function_param_descriptions . get ( name , None )
2025-04-11 12:22:14 +08:00
2025-05-01 02:22:38 +08:00
if param_description :
2025-06-05 08:58:22 +08:00
field_defs [ name ] = (
type_hint ,
Field ( default_value , description = param_description ) ,
2025-05-04 03:48:24 +08:00
)
2025-04-11 12:22:14 +08:00
else :
2024-11-22 01:19:56 +08:00
field_defs [ name ] = type_hint , default_value
2024-11-23 00:25:52 +08:00
model = create_model ( func . __name__ , * * field_defs )
2025-05-01 02:22:38 +08:00
model . __doc__ = function_description
2024-11-23 00:25:52 +08:00
return model
2024-11-22 01:19:56 +08:00
2025-04-11 10:41:17 +08:00
def get_functions_from_tool ( tool : object ) - > list [ Callable ] :
2024-11-22 01:19:56 +08:00
return [
getattr ( tool , func )
for func in dir ( tool )
2025-04-11 10:41:17 +08:00
if callable (
getattr ( tool , func )
) # checks if the attribute is callable (a method or function).
and not func . startswith (
" __ "
) # filters out special (dunder) methods like init, str, etc. — these are usually built-in functions of an object that you might not need to use directly.
and not inspect . isclass (
getattr ( tool , func )
) # ensures that the callable is not a class itself, just a method or function.
2024-06-11 11:39:55 +08:00
]
2025-04-11 10:41:17 +08:00
def get_tool_specs ( tool_module : object ) - > list [ dict ] :
function_models = map (
2025-04-11 10:43:26 +08:00
convert_function_to_pydantic_model , get_functions_from_tool ( tool_module )
2025-04-05 19:03:15 +08:00
)
2025-04-11 10:41:17 +08:00
2025-04-11 12:22:14 +08:00
specs = [
2025-04-11 10:43:26 +08:00
convert_pydantic_model_to_openai_function_spec ( function_model )
for function_model in function_models
2025-04-05 18:59:11 +08:00
]
2025-04-05 18:05:52 +08:00
2025-04-11 12:22:14 +08:00
return specs
2025-04-05 18:05:52 +08:00
def resolve_schema ( schema , components ) :
"""
Recursively resolves a JSON schema using OpenAPI components .
"""
if not schema :
return { }
if " $ref " in schema :
ref_path = schema [ " $ref " ]
ref_parts = ref_path . strip ( " #/ " ) . split ( " / " )
resolved = components
for part in ref_parts [ 1 : ] : # Skip the initial 'components'
resolved = resolved . get ( part , { } )
return resolve_schema ( resolved , components )
resolved_schema = copy . deepcopy ( schema )
# Recursively resolve inner schemas
if " properties " in resolved_schema :
for prop , prop_schema in resolved_schema [ " properties " ] . items ( ) :
resolved_schema [ " properties " ] [ prop ] = resolve_schema (
prop_schema , components
)
if " items " in resolved_schema :
resolved_schema [ " items " ] = resolve_schema ( resolved_schema [ " items " ] , components )
return resolved_schema
def convert_openapi_to_tool_payload ( openapi_spec ) :
"""
Converts an OpenAPI specification into a custom tool payload structure .
Args :
openapi_spec ( dict ) : The OpenAPI specification as a Python dict .
Returns :
list : A list of tool payloads .
"""
tool_payload = [ ]
for path , methods in openapi_spec . get ( " paths " , { } ) . items ( ) :
for method , operation in methods . items ( ) :
2025-04-19 18:46:06 +08:00
if operation . get ( " operationId " ) :
tool = {
" name " : operation . get ( " operationId " ) ,
" description " : operation . get (
" description " ,
operation . get ( " summary " , " No description available. " ) ,
) ,
" parameters " : { " type " : " object " , " properties " : { } , " required " : [ ] } ,
2025-04-05 18:05:52 +08:00
}
2025-04-19 18:46:06 +08:00
# Extract path and query parameters
for param in operation . get ( " parameters " , [ ] ) :
param_name = param [ " name " ]
param_schema = param . get ( " schema " , { } )
description = param_schema . get ( " description " , " " )
if not description :
description = param . get ( " description " ) or " "
if param_schema . get ( " enum " ) and isinstance (
param_schema . get ( " enum " ) , list
) :
description + = (
f " . Possible values: { ' , ' . join ( param_schema . get ( ' enum ' ) ) } "
)
2025-08-07 13:21:22 +08:00
param_property = {
2025-04-19 18:46:06 +08:00
" type " : param_schema . get ( " type " ) ,
" description " : description ,
}
2025-08-08 22:09:31 +08:00
2025-08-07 13:21:22 +08:00
# Include items property for array types (required by OpenAI)
if param_schema . get ( " type " ) == " array " and " items " in param_schema :
param_property [ " items " ] = param_schema [ " items " ]
2025-08-08 22:09:31 +08:00
2025-08-07 13:21:22 +08:00
tool [ " parameters " ] [ " properties " ] [ param_name ] = param_property
2025-04-19 18:46:06 +08:00
if param . get ( " required " ) :
tool [ " parameters " ] [ " required " ] . append ( param_name )
# Extract and resolve requestBody if available
request_body = operation . get ( " requestBody " )
if request_body :
content = request_body . get ( " content " , { } )
json_schema = content . get ( " application/json " , { } ) . get ( " schema " )
if json_schema :
resolved_schema = resolve_schema (
json_schema , openapi_spec . get ( " components " , { } )
2025-04-05 18:05:52 +08:00
)
2025-04-19 18:46:06 +08:00
if resolved_schema . get ( " properties " ) :
tool [ " parameters " ] [ " properties " ] . update (
resolved_schema [ " properties " ]
)
if " required " in resolved_schema :
tool [ " parameters " ] [ " required " ] = list (
set (
tool [ " parameters " ] [ " required " ]
+ resolved_schema [ " required " ]
)
2025-04-05 18:05:52 +08:00
)
2025-04-19 18:46:06 +08:00
elif resolved_schema . get ( " type " ) == " array " :
tool [ " parameters " ] = (
resolved_schema # special case for array
2025-04-05 18:05:52 +08:00
)
2025-04-19 18:46:06 +08:00
tool_payload . append ( tool )
2025-04-05 18:05:52 +08:00
return tool_payload
2025-08-19 00:53:46 +08:00
async def set_tool_servers ( request : Request ) :
request . app . state . TOOL_SERVERS = await get_tool_servers_data (
request . app . state . config . TOOL_SERVER_CONNECTIONS
)
if request . app . state . redis is not None :
2025-08-19 01:28:28 +08:00
await request . app . state . redis . set (
" tool_servers " , json . dumps ( request . app . state . TOOL_SERVERS )
2025-08-19 00:53:46 +08:00
)
return request . app . state . TOOL_SERVERS
async def get_tool_servers ( request : Request ) :
tool_servers = [ ]
if request . app . state . redis is not None :
2025-08-19 01:28:28 +08:00
try :
tool_servers = json . loads ( await request . app . state . redis . get ( " tool_servers " ) )
except Exception as e :
log . error ( f " Error fetching tool_servers from Redis: { e } " )
2025-08-19 00:53:46 +08:00
if not tool_servers :
await set_tool_servers ( request )
request . app . state . TOOL_SERVERS = tool_servers
return request . app . state . TOOL_SERVERS
2025-04-05 18:05:52 +08:00
async def get_tool_server_data ( token : str , url : str ) - > Dict [ str , Any ] :
headers = {
" Accept " : " application/json " ,
" Content-Type " : " application/json " ,
}
if token :
headers [ " Authorization " ] = f " Bearer { token } "
error = None
try :
2025-04-11 01:23:14 +08:00
timeout = aiohttp . ClientTimeout ( total = AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA )
2025-04-18 13:02:16 +08:00
async with aiohttp . ClientSession ( timeout = timeout , trust_env = True ) as session :
async with session . get (
url , headers = headers , ssl = AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL
) as response :
2025-04-05 18:05:52 +08:00
if response . status != 200 :
error_body = await response . json ( )
raise Exception ( error_body )
2025-04-08 21:11:37 +08:00
# 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 :
res = await response . json ( )
2025-04-05 18:05:52 +08:00
except Exception as err :
2025-04-08 21:13:58 +08:00
log . exception ( f " Could not fetch tool server spec from { url } " )
2025-04-05 18:05:52 +08:00
if isinstance ( err , dict ) and " detail " in err :
error = err [ " detail " ]
else :
error = str ( err )
raise Exception ( error )
data = {
" openapi " : res ,
" info " : res . get ( " info " , { } ) ,
" specs " : convert_openapi_to_tool_payload ( res ) ,
}
2025-06-11 21:36:36 +08:00
log . info ( f " Fetched data: { data } " )
2025-04-05 18:05:52 +08:00
return data
2025-04-05 18:40:01 +08:00
async def get_tool_servers_data (
servers : List [ Dict [ str , Any ] ] , session_token : Optional [ str ] = None
) - > List [ Dict [ str , Any ] ] :
# Prepare list of enabled servers along with their original index
server_entries = [ ]
for idx , server in enumerate ( servers ) :
if server . get ( " config " , { } ) . get ( " enable " ) :
2025-05-27 04:20:47 +08:00
# Path (to OpenAPI spec URL) can be either a full URL or a path to append to the base URL
openapi_path = server . get ( " path " , " openapi.json " )
2025-07-31 20:47:02 +08:00
full_url = get_tool_server_url ( server . get ( " url " ) , openapi_path )
2025-04-05 18:05:52 +08:00
2025-05-27 04:10:33 +08:00
info = server . get ( " info " , { } )
2025-04-05 18:40:01 +08:00
auth_type = server . get ( " auth_type " , " bearer " )
token = None
2025-04-05 18:05:52 +08:00
2025-04-05 18:40:01 +08:00
if auth_type == " bearer " :
token = server . get ( " key " , " " )
elif auth_type == " session " :
token = session_token
2025-08-19 00:38:55 +08:00
2025-08-19 01:28:28 +08:00
id = info . get ( " id " )
if not id :
id = str ( idx )
2025-08-19 00:38:55 +08:00
server_entries . append ( ( id , idx , server , full_url , info , token ) )
2025-04-05 18:05:52 +08:00
2025-04-05 18:40:01 +08:00
# Create async tasks to fetch data
2025-05-27 04:10:33 +08:00
tasks = [
2025-08-19 00:38:55 +08:00
get_tool_server_data ( token , url ) for ( _ , _ , _ , url , _ , token ) in server_entries
2025-05-27 04:10:33 +08:00
]
2025-04-05 18:05:52 +08:00
2025-04-05 18:40:01 +08:00
# Execute tasks concurrently
2025-04-05 18:05:52 +08:00
responses = await asyncio . gather ( * tasks , return_exceptions = True )
2025-04-05 18:40:01 +08:00
# Build final results with index and server metadata
results = [ ]
2025-08-19 00:38:55 +08:00
for ( id , idx , server , url , info , _ ) , response in zip ( server_entries , responses ) :
2025-04-05 18:05:52 +08:00
if isinstance ( response , Exception ) :
2025-05-12 22:18:47 +08:00
log . error ( f " Failed to connect to { url } OpenAPI tool server " )
2025-04-05 18:40:01 +08:00
continue
2025-05-27 04:10:33 +08:00
openapi_data = response . get ( " openapi " , { } )
if info and isinstance ( openapi_data , dict ) :
2025-08-02 17:59:07 +08:00
openapi_data [ " info " ] = openapi_data . get ( " info " , { } )
2025-05-27 04:10:33 +08:00
if " name " in info :
openapi_data [ " info " ] [ " title " ] = info . get ( " name " , " Tool Server " )
if " description " in info :
openapi_data [ " info " ] [ " description " ] = info . get ( " description " , " " )
2025-04-05 18:40:01 +08:00
results . append (
{
2025-08-19 00:38:55 +08:00
" id " : str ( id ) ,
2025-04-05 18:40:01 +08:00
" idx " : idx ,
" url " : server . get ( " url " ) ,
2025-05-27 04:10:33 +08:00
" openapi " : openapi_data ,
2025-04-05 18:40:01 +08:00
" info " : response . get ( " info " ) ,
" specs " : response . get ( " specs " ) ,
}
)
2025-04-05 18:05:52 +08:00
return results
async def execute_tool_server (
token : str , url : str , name : str , params : Dict [ str , Any ] , server_data : Dict [ str , Any ]
) - > Any :
error = None
try :
openapi = server_data . get ( " openapi " , { } )
paths = openapi . get ( " paths " , { } )
matching_route = None
for route_path , methods in paths . items ( ) :
for http_method , operation in methods . items ( ) :
if isinstance ( operation , dict ) and operation . get ( " operationId " ) == name :
matching_route = ( route_path , methods )
break
if matching_route :
break
if not matching_route :
raise Exception ( f " No matching route found for operationId: { name } " )
route_path , methods = matching_route
method_entry = None
for http_method , operation in methods . items ( ) :
if operation . get ( " operationId " ) == name :
method_entry = ( http_method . lower ( ) , operation )
break
if not method_entry :
raise Exception ( f " No matching method found for operationId: { name } " )
http_method , operation = method_entry
path_params = { }
query_params = { }
body_params = { }
for param in operation . get ( " parameters " , [ ] ) :
param_name = param [ " name " ]
param_in = param [ " in " ]
if param_name in params :
if param_in == " path " :
path_params [ param_name ] = params [ param_name ]
elif param_in == " query " :
query_params [ param_name ] = params [ param_name ]
final_url = f " { url } { route_path } "
for key , value in path_params . items ( ) :
final_url = final_url . replace ( f " {{ { key } }} " , str ( value ) )
if query_params :
query_string = " & " . join ( f " { k } = { v } " for k , v in query_params . items ( ) )
final_url = f " { final_url } ? { query_string } "
if operation . get ( " requestBody " , { } ) . get ( " content " ) :
if params :
body_params = params
else :
raise Exception (
f " Request body expected for operation ' { name } ' but none found. "
)
headers = { " Content-Type " : " application/json " }
if token :
headers [ " Authorization " ] = f " Bearer { token } "
2025-08-12 18:29:02 +08:00
async with aiohttp . ClientSession (
trust_env = True , timeout = aiohttp . ClientTimeout ( total = AIOHTTP_CLIENT_TIMEOUT )
) as session :
2025-04-05 18:05:52 +08:00
request_method = getattr ( session , http_method . lower ( ) )
if http_method in [ " post " , " put " , " patch " ] :
async with request_method (
2025-04-18 13:02:16 +08:00
final_url ,
json = body_params ,
headers = headers ,
ssl = AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL ,
2025-04-05 18:05:52 +08:00
) as response :
if response . status > = 400 :
text = await response . text ( )
raise Exception ( f " HTTP error { response . status } : { text } " )
2025-08-11 21:36:36 +08:00
try :
response_data = await response . json ( )
except Exception :
response_data = await response . text ( )
return response_data
2025-04-05 18:05:52 +08:00
else :
2025-04-18 13:02:16 +08:00
async with request_method (
final_url ,
headers = headers ,
ssl = AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL ,
) as response :
2025-04-05 18:05:52 +08:00
if response . status > = 400 :
text = await response . text ( )
raise Exception ( f " HTTP error { response . status } : { text } " )
2025-08-11 21:36:36 +08:00
try :
response_data = await response . json ( )
except Exception :
response_data = await response . text ( )
return response_data
2025-04-05 18:05:52 +08:00
except Exception as err :
error = str ( err )
2025-06-11 21:36:36 +08:00
log . exception ( f " API Request Error: { error } " )
2025-04-05 18:05:52 +08:00
return { " error " : error }
2025-07-28 22:32:05 +08:00
2025-07-31 20:47:02 +08:00
def get_tool_server_url ( url : Optional [ str ] , path : str ) - > str :
2025-07-28 22:32:05 +08:00
"""
Build the full URL for a tool server , given a base url and a path .
"""
if " :// " in path :
# If it contains "://", it's a full URL
return path
if not path . startswith ( " / " ) :
# Ensure the path starts with a slash
path = f " / { path } "
return f " { url } { path } "