2024-02-01 18:11:57 +08:00
import json
2025-02-17 17:05:13 +08:00
from collections . abc import Generator
2024-04-12 17:46:39 +08:00
from os import getenv
2025-02-17 17:05:13 +08:00
from typing import Any , Optional
2024-03-04 14:16:47 +08:00
from urllib . parse import urlencode
2024-01-23 19:58:23 +08:00
2024-02-01 18:11:57 +08:00
import httpx
2024-02-06 13:21:13 +08:00
2024-11-20 13:26:42 +08:00
from core . file . file_manager import download
2024-09-13 22:42:08 +08:00
from core . helper import ssrf_proxy
2025-02-17 17:05:13 +08:00
from core . tools . __base . tool import Tool
from core . tools . __base . tool_runtime import ToolRuntime
2024-05-27 22:01:11 +08:00
from core . tools . entities . tool_bundle import ApiToolBundle
2025-02-17 17:05:13 +08:00
from core . tools . entities . tool_entities import ToolEntity , ToolInvokeMessage , ToolProviderType
2024-03-19 18:17:12 +08:00
from core . tools . errors import ToolInvokeError , ToolParameterValidationError , ToolProviderCredentialValidationError
2024-01-23 19:58:23 +08:00
2024-04-12 17:46:39 +08:00
API_TOOL_DEFAULT_TIMEOUT = (
2024-09-10 17:00:20 +08:00
int ( getenv ( " API_TOOL_DEFAULT_CONNECT_TIMEOUT " , " 10 " ) ) ,
int ( getenv ( " API_TOOL_DEFAULT_READ_TIMEOUT " , " 60 " ) ) ,
2024-04-12 17:46:39 +08:00
)
2024-01-23 19:58:23 +08:00
2024-06-24 16:14:59 +08:00
2024-01-23 19:58:23 +08:00
class ApiTool ( Tool ) :
2024-05-27 22:01:11 +08:00
api_bundle : ApiToolBundle
2025-02-17 17:05:13 +08:00
provider_id : str
2024-06-24 16:14:59 +08:00
2024-01-23 19:58:23 +08:00
"""
Api tool
"""
2024-06-24 16:14:59 +08:00
2025-02-17 17:05:13 +08:00
def __init__ ( self , entity : ToolEntity , api_bundle : ApiToolBundle , runtime : ToolRuntime , provider_id : str ) :
super ( ) . __init__ ( entity , runtime )
self . api_bundle = api_bundle
self . provider_id = provider_id
def fork_tool_runtime ( self , runtime : ToolRuntime ) :
2024-01-23 19:58:23 +08:00
"""
2024-09-10 17:00:20 +08:00
fork a new tool with meta data
2024-01-23 19:58:23 +08:00
2024-09-10 17:00:20 +08:00
: param meta : the meta data of a tool call processing , tenant_id is required
: return : the new tool
2024-01-23 19:58:23 +08:00
"""
2024-12-24 18:38:51 +08:00
if self . api_bundle is None :
raise ValueError ( " api_bundle is required " )
2024-01-23 19:58:23 +08:00
return self . __class__ (
2025-02-17 17:05:13 +08:00
entity = self . entity ,
2024-12-24 18:38:51 +08:00
api_bundle = self . api_bundle . model_copy ( ) ,
2025-02-17 17:05:13 +08:00
runtime = runtime ,
provider_id = self . provider_id ,
2024-01-23 19:58:23 +08:00
)
2024-06-24 16:14:59 +08:00
2024-09-10 17:00:20 +08:00
def validate_credentials (
self , credentials : dict [ str , Any ] , parameters : dict [ str , Any ] , format_only : bool = False
) - > str :
2024-01-23 19:58:23 +08:00
"""
2024-09-10 17:00:20 +08:00
validate the credentials for Api tool
2024-01-23 19:58:23 +08:00
"""
2024-09-10 17:00:20 +08:00
# assemble validate request and request parameters
2024-01-23 19:58:23 +08:00
headers = self . assembling_request ( parameters )
if format_only :
2024-09-10 17:00:20 +08:00
return " "
2024-01-23 19:58:23 +08:00
response = self . do_http_request ( self . api_bundle . server_url , self . api_bundle . method , headers , parameters )
# validate response
2024-02-05 18:48:30 +08:00
return self . validate_and_parse_response ( response )
2024-01-23 19:58:23 +08:00
2024-04-08 18:51:46 +08:00
def tool_provider_type ( self ) - > ToolProviderType :
2024-05-27 22:01:11 +08:00
return ToolProviderType . API
2024-04-08 18:51:46 +08:00
2024-02-09 15:21:33 +08:00
def assembling_request ( self , parameters : dict [ str , Any ] ) - > dict [ str , Any ] :
2025-02-17 17:05:13 +08:00
if self . runtime is None :
raise ToolProviderCredentialValidationError ( " runtime not initialized " )
2024-01-23 19:58:23 +08:00
headers = { }
2024-12-24 18:38:51 +08:00
if self . runtime is None :
raise ValueError ( " runtime is required " )
2024-01-23 19:58:23 +08:00
credentials = self . runtime . credentials or { }
2024-09-10 17:00:20 +08:00
if " auth_type " not in credentials :
raise ToolProviderCredentialValidationError ( " Missing auth_type " )
2024-01-23 19:58:23 +08:00
2024-09-10 17:00:20 +08:00
if credentials [ " auth_type " ] == " api_key " :
api_key_header = " api_key "
2024-01-23 19:58:23 +08:00
2024-09-10 17:00:20 +08:00
if " api_key_header " in credentials :
api_key_header = credentials [ " api_key_header " ]
2024-06-24 16:14:59 +08:00
2024-09-10 17:00:20 +08:00
if " api_key_value " not in credentials :
raise ToolProviderCredentialValidationError ( " Missing api_key_value " )
elif not isinstance ( credentials [ " api_key_value " ] , str ) :
raise ToolProviderCredentialValidationError ( " api_key_value must be a string " )
2024-06-24 16:14:59 +08:00
2024-09-10 17:00:20 +08:00
if " api_key_header_prefix " in credentials :
api_key_header_prefix = credentials [ " api_key_header_prefix " ]
if api_key_header_prefix == " basic " and credentials [ " api_key_value " ] :
2025-01-21 10:12:29 +08:00
credentials [ " api_key_value " ] = f " Basic { credentials [ ' api_key_value ' ] } "
2024-09-10 17:00:20 +08:00
elif api_key_header_prefix == " bearer " and credentials [ " api_key_value " ] :
2025-01-21 10:12:29 +08:00
credentials [ " api_key_value " ] = f " Bearer { credentials [ ' api_key_value ' ] } "
2024-09-10 17:00:20 +08:00
elif api_key_header_prefix == " custom " :
2024-02-28 23:19:08 +08:00
pass
2024-06-24 16:14:59 +08:00
2024-09-10 17:00:20 +08:00
headers [ api_key_header ] = credentials [ " api_key_value " ]
2024-01-23 19:58:23 +08:00
2024-12-24 18:38:51 +08:00
needed_parameters = [ parameter for parameter in ( self . api_bundle . parameters or [ ] ) if parameter . required ]
2024-01-23 19:58:23 +08:00
for parameter in needed_parameters :
if parameter . required and parameter . name not in parameters :
2024-03-19 18:17:12 +08:00
raise ToolParameterValidationError ( f " Missing required parameter { parameter . name } " )
2024-06-24 16:14:59 +08:00
2024-01-23 19:58:23 +08:00
if parameter . default is not None and parameter . name not in parameters :
parameters [ parameter . name ] = parameter . default
return headers
2024-06-24 16:14:59 +08:00
def validate_and_parse_response ( self , response : httpx . Response ) - > str :
2024-01-23 19:58:23 +08:00
"""
2024-09-10 17:00:20 +08:00
validate the response
2024-01-23 19:58:23 +08:00
"""
if isinstance ( response , httpx . Response ) :
if response . status_code > = 400 :
2024-03-19 18:17:12 +08:00
raise ToolInvokeError ( f " Request failed with status code { response . status_code } and { response . text } " )
2024-01-30 22:22:58 +08:00
if not response . content :
2024-09-10 17:00:20 +08:00
return " Empty response from the tool, please check your parameters and try again. "
2024-01-30 22:22:58 +08:00
try :
response = response . json ( )
try :
return json . dumps ( response , ensure_ascii = False )
2025-02-17 17:05:13 +08:00
except Exception :
2024-01-30 22:22:58 +08:00
return json . dumps ( response )
2025-02-17 17:05:13 +08:00
except Exception :
2024-01-30 22:22:58 +08:00
return response . text
2024-01-23 19:58:23 +08:00
else :
2024-09-10 17:00:20 +08:00
raise ValueError ( f " Invalid response type { type ( response ) } " )
2024-06-24 16:14:59 +08:00
@staticmethod
def get_parameter_value ( parameter , parameters ) :
2024-09-10 17:00:20 +08:00
if parameter [ " name " ] in parameters :
return parameters [ parameter [ " name " ] ]
elif parameter . get ( " required " , False ) :
2024-06-24 16:14:59 +08:00
raise ToolParameterValidationError ( f " Missing required parameter { parameter [ ' name ' ] } " )
else :
2024-09-10 17:00:20 +08:00
return ( parameter . get ( " schema " , { } ) or { } ) . get ( " default " , " " )
2024-06-24 16:14:59 +08:00
2024-09-10 17:00:20 +08:00
def do_http_request (
self , url : str , method : str , headers : dict [ str , Any ] , parameters : dict [ str , Any ]
) - > httpx . Response :
2024-01-23 19:58:23 +08:00
"""
2024-09-10 17:00:20 +08:00
do http request depending on api bundle
2024-01-23 19:58:23 +08:00
"""
method = method . lower ( )
params = { }
path_params = { }
2024-12-24 18:38:51 +08:00
# FIXME: body should be a dict[str, Any] but it changed a lot in this function
body : Any = { }
2024-01-23 19:58:23 +08:00
cookies = { }
2024-11-20 13:26:42 +08:00
files = [ ]
2024-01-23 19:58:23 +08:00
# check parameters
2024-09-10 17:00:20 +08:00
for parameter in self . api_bundle . openapi . get ( " parameters " , [ ] ) :
2024-06-24 16:14:59 +08:00
value = self . get_parameter_value ( parameter , parameters )
2024-09-10 17:00:20 +08:00
if parameter [ " in " ] == " path " :
path_params [ parameter [ " name " ] ] = value
2024-01-23 19:58:23 +08:00
2024-09-10 17:00:20 +08:00
elif parameter [ " in " ] == " query " :
if value != " " :
params [ parameter [ " name " ] ] = value
2024-01-23 19:58:23 +08:00
2024-09-10 17:00:20 +08:00
elif parameter [ " in " ] == " cookie " :
cookies [ parameter [ " name " ] ] = value
2024-01-23 19:58:23 +08:00
2024-09-10 17:00:20 +08:00
elif parameter [ " in " ] == " header " :
headers [ parameter [ " name " ] ] = value
2024-01-23 19:58:23 +08:00
# check if there is a request body and handle it
2024-09-10 17:00:20 +08:00
if " requestBody " in self . api_bundle . openapi and self . api_bundle . openapi [ " requestBody " ] is not None :
2024-01-23 19:58:23 +08:00
# handle json request body
2024-09-10 17:00:20 +08:00
if " content " in self . api_bundle . openapi [ " requestBody " ] :
for content_type in self . api_bundle . openapi [ " requestBody " ] [ " content " ] :
headers [ " Content-Type " ] = content_type
body_schema = self . api_bundle . openapi [ " requestBody " ] [ " content " ] [ content_type ] [ " schema " ]
required = body_schema . get ( " required " , [ ] )
properties = body_schema . get ( " properties " , { } )
2024-01-23 19:58:23 +08:00
for name , property in properties . items ( ) :
if name in parameters :
2025-02-19 14:44:18 +08:00
# multiple file upload: if the type is array and the items have format as binary
if property . get ( " type " ) == " array " and property . get ( " items " , { } ) . get ( " format " ) == " binary " :
# parameters[name] should be a list of file objects.
for f in parameters [ name ] :
files . append ( ( name , ( f . filename , download ( f ) , f . mime_type ) ) )
elif property . get ( " format " ) == " binary " :
2024-11-20 13:26:42 +08:00
f = parameters [ name ]
files . append ( ( name , ( f . filename , download ( f ) , f . mime_type ) ) )
else :
# convert type
body [ name ] = self . _convert_body_property_type ( property , parameters [ name ] )
2024-01-23 19:58:23 +08:00
elif name in required :
2024-03-19 18:17:12 +08:00
raise ToolParameterValidationError (
2024-01-23 19:58:23 +08:00
f " Missing required parameter { name } in operation { self . api_bundle . operation_id } "
)
2024-09-10 17:00:20 +08:00
elif " default " in property :
body [ name ] = property [ " default " ]
2024-01-23 19:58:23 +08:00
else :
body [ name ] = None
break
2024-06-24 16:14:59 +08:00
2024-01-23 19:58:23 +08:00
# replace path parameters
for name , value in path_params . items ( ) :
2024-09-10 17:00:20 +08:00
url = url . replace ( f " {{ { name } }} " , f " { value } " )
2024-01-23 19:58:23 +08:00
2024-11-20 13:26:42 +08:00
# parse http body data if needed
2024-09-10 17:00:20 +08:00
if " Content-Type " in headers :
if headers [ " Content-Type " ] == " application/json " :
2024-06-24 16:14:59 +08:00
body = json . dumps ( body )
2024-09-10 17:00:20 +08:00
elif headers [ " Content-Type " ] == " application/x-www-form-urlencoded " :
2024-03-04 14:16:47 +08:00
body = urlencode ( body )
2024-01-23 19:58:23 +08:00
else :
body = body
2025-02-19 14:41:27 +08:00
# if there is a file upload, remove the Content-Type header so that httpx can automatically generate the boundary header required for multipart/form-data.
# issue: https://github.com/langgenius/dify/issues/13684
# reference: https://stackoverflow.com/questions/39280438/fetch-missing-boundary-in-multipart-form-data-post
if files :
headers . pop ( " Content-Type " , None )
2024-06-24 16:14:59 +08:00
2025-01-06 20:35:53 +08:00
if method in {
" get " ,
" head " ,
" post " ,
" put " ,
" delete " ,
" patch " ,
" options " ,
" GET " ,
" POST " ,
" PUT " ,
" PATCH " ,
" DELETE " ,
" HEAD " ,
" OPTIONS " ,
} :
response : httpx . Response = getattr ( ssrf_proxy , method . lower ( ) ) (
2024-09-10 17:00:20 +08:00
url ,
params = params ,
headers = headers ,
cookies = cookies ,
data = body ,
2024-11-20 13:26:42 +08:00
files = files ,
2024-09-10 17:00:20 +08:00
timeout = API_TOOL_DEFAULT_TIMEOUT ,
follow_redirects = True ,
)
2024-06-24 16:14:59 +08:00
return response
2024-01-23 19:58:23 +08:00
else :
2024-12-21 21:24:59 +08:00
raise ValueError ( f " Invalid http method { method } " )
2024-06-24 16:14:59 +08:00
2024-09-10 17:00:20 +08:00
def _convert_body_property_any_of (
self , property : dict [ str , Any ] , value : Any , any_of : list [ dict [ str , Any ] ] , max_recursive = 10
) - > Any :
2024-02-29 14:39:05 +08:00
if max_recursive < = 0 :
raise Exception ( " Max recursion depth reached " )
for option in any_of or [ ] :
try :
2024-09-10 17:00:20 +08:00
if " type " in option :
2024-02-29 14:39:05 +08:00
# Attempt to convert the value based on the type.
2024-09-10 17:00:20 +08:00
if option [ " type " ] == " integer " or option [ " type " ] == " int " :
2024-02-29 14:39:05 +08:00
return int ( value )
2024-09-10 17:00:20 +08:00
elif option [ " type " ] == " number " :
if " . " in str ( value ) :
2024-02-29 14:39:05 +08:00
return float ( value )
else :
return int ( value )
2024-09-10 17:00:20 +08:00
elif option [ " type " ] == " string " :
2024-02-29 14:39:05 +08:00
return str ( value )
2024-09-10 17:00:20 +08:00
elif option [ " type " ] == " boolean " :
2024-09-13 22:42:08 +08:00
if str ( value ) . lower ( ) in { " true " , " 1 " } :
2024-02-29 14:39:05 +08:00
return True
2024-09-13 22:42:08 +08:00
elif str ( value ) . lower ( ) in { " false " , " 0 " } :
2024-02-29 14:39:05 +08:00
return False
else :
continue # Not a boolean, try next option
2024-09-10 17:00:20 +08:00
elif option [ " type " ] == " null " and not value :
2024-02-29 14:39:05 +08:00
return None
else :
continue # Unsupported type, try next option
2024-09-10 17:00:20 +08:00
elif " anyOf " in option and isinstance ( option [ " anyOf " ] , list ) :
2024-02-29 14:39:05 +08:00
# Recursive call to handle nested anyOf
2024-09-10 17:00:20 +08:00
return self . _convert_body_property_any_of ( property , value , option [ " anyOf " ] , max_recursive - 1 )
2024-02-29 14:39:05 +08:00
except ValueError :
continue # Conversion failed, try next option
# If no option succeeded, you might want to return the value as is or raise an error
return value # or raise ValueError(f"Cannot convert value '{value}' to any specified type in anyOf")
def _convert_body_property_type ( self , property : dict [ str , Any ] , value : Any ) - > Any :
try :
2024-09-10 17:00:20 +08:00
if " type " in property :
if property [ " type " ] == " integer " or property [ " type " ] == " int " :
2024-02-29 14:39:05 +08:00
return int ( value )
2024-09-10 17:00:20 +08:00
elif property [ " type " ] == " number " :
2024-02-29 14:39:05 +08:00
# check if it is a float
2024-09-10 17:00:20 +08:00
if " . " in str ( value ) :
2024-02-29 14:39:05 +08:00
return float ( value )
else :
return int ( value )
2024-09-10 17:00:20 +08:00
elif property [ " type " ] == " string " :
2024-02-29 14:39:05 +08:00
return str ( value )
2024-09-10 17:00:20 +08:00
elif property [ " type " ] == " boolean " :
2024-02-29 14:39:05 +08:00
return bool ( value )
2024-09-10 17:00:20 +08:00
elif property [ " type " ] == " null " :
2024-02-29 14:39:05 +08:00
if value is None :
return None
2024-09-10 17:00:20 +08:00
elif property [ " type " ] == " object " or property [ " type " ] == " array " :
2024-04-16 19:54:17 +08:00
if isinstance ( value , str ) :
try :
return json . loads ( value )
except ValueError :
return value
elif isinstance ( value , dict ) :
return value
else :
return value
2024-02-29 14:39:05 +08:00
else :
raise ValueError ( f " Invalid type { property [ ' type ' ] } for property { property } " )
2024-09-10 17:00:20 +08:00
elif " anyOf " in property and isinstance ( property [ " anyOf " ] , list ) :
return self . _convert_body_property_any_of ( property , value , property [ " anyOf " ] )
2025-02-17 17:05:13 +08:00
except ValueError :
2024-02-29 14:39:05 +08:00
return value
2024-01-23 19:58:23 +08:00
2025-02-17 17:05:13 +08:00
def _invoke (
self ,
user_id : str ,
tool_parameters : dict [ str , Any ] ,
conversation_id : Optional [ str ] = None ,
app_id : Optional [ str ] = None ,
message_id : Optional [ str ] = None ,
) - > Generator [ ToolInvokeMessage , None , None ] :
2024-01-23 19:58:23 +08:00
"""
invoke http request
"""
2024-12-24 18:38:51 +08:00
response : httpx . Response | str = " "
2024-01-23 19:58:23 +08:00
# assemble request
2024-01-31 11:58:07 +08:00
headers = self . assembling_request ( tool_parameters )
2024-01-23 19:58:23 +08:00
# do http request
2024-01-31 11:58:07 +08:00
response = self . do_http_request ( self . api_bundle . server_url , self . api_bundle . method , headers , tool_parameters )
2024-01-23 19:58:23 +08:00
# validate response
response = self . validate_and_parse_response ( response )
# assemble invoke message
2025-02-17 17:05:13 +08:00
yield self . create_text_message ( response )