api_tool.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import json
  2. from json import dumps
  3. from typing import Any, Union
  4. import httpx
  5. import requests
  6. import core.helper.ssrf_proxy as ssrf_proxy
  7. from core.tools.entities.tool_bundle import ApiBasedToolBundle
  8. from core.tools.entities.tool_entities import ToolInvokeMessage
  9. from core.tools.errors import ToolProviderCredentialValidationError
  10. from core.tools.tool.tool import Tool
  11. class ApiTool(Tool):
  12. api_bundle: ApiBasedToolBundle
  13. """
  14. Api tool
  15. """
  16. def fork_tool_runtime(self, meta: dict[str, Any]) -> 'Tool':
  17. """
  18. fork a new tool with meta data
  19. :param meta: the meta data of a tool call processing, tenant_id is required
  20. :return: the new tool
  21. """
  22. return self.__class__(
  23. identity=self.identity.copy() if self.identity else None,
  24. parameters=self.parameters.copy() if self.parameters else None,
  25. description=self.description.copy() if self.description else None,
  26. api_bundle=self.api_bundle.copy() if self.api_bundle else None,
  27. runtime=Tool.Runtime(**meta)
  28. )
  29. def validate_credentials(self, credentials: dict[str, Any], parameters: dict[str, Any], format_only: bool = False) -> str:
  30. """
  31. validate the credentials for Api tool
  32. """
  33. # assemble validate request and request parameters
  34. headers = self.assembling_request(parameters)
  35. if format_only:
  36. return
  37. response = self.do_http_request(self.api_bundle.server_url, self.api_bundle.method, headers, parameters)
  38. # validate response
  39. return self.validate_and_parse_response(response)
  40. def assembling_request(self, parameters: dict[str, Any]) -> dict[str, Any]:
  41. headers = {}
  42. credentials = self.runtime.credentials or {}
  43. if 'auth_type' not in credentials:
  44. raise ToolProviderCredentialValidationError('Missing auth_type')
  45. if credentials['auth_type'] == 'api_key':
  46. api_key_header = 'api_key'
  47. if 'api_key_header' in credentials:
  48. api_key_header = credentials['api_key_header']
  49. if 'api_key_value' not in credentials:
  50. raise ToolProviderCredentialValidationError('Missing api_key_value')
  51. headers[api_key_header] = credentials['api_key_value']
  52. needed_parameters = [parameter for parameter in self.api_bundle.parameters if parameter.required]
  53. for parameter in needed_parameters:
  54. if parameter.required and parameter.name not in parameters:
  55. raise ToolProviderCredentialValidationError(f"Missing required parameter {parameter.name}")
  56. if parameter.default is not None and parameter.name not in parameters:
  57. parameters[parameter.name] = parameter.default
  58. return headers
  59. def validate_and_parse_response(self, response: Union[httpx.Response, requests.Response]) -> str:
  60. """
  61. validate the response
  62. """
  63. if isinstance(response, httpx.Response):
  64. if response.status_code >= 400:
  65. raise ToolProviderCredentialValidationError(f"Request failed with status code {response.status_code}")
  66. if not response.content:
  67. return 'Empty response from the tool, please check your parameters and try again.'
  68. try:
  69. response = response.json()
  70. try:
  71. return json.dumps(response, ensure_ascii=False)
  72. except Exception as e:
  73. return json.dumps(response)
  74. except Exception as e:
  75. return response.text
  76. elif isinstance(response, requests.Response):
  77. if not response.ok:
  78. raise ToolProviderCredentialValidationError(f"Request failed with status code {response.status_code}")
  79. if not response.content:
  80. return 'Empty response from the tool, please check your parameters and try again.'
  81. try:
  82. response = response.json()
  83. try:
  84. return json.dumps(response, ensure_ascii=False)
  85. except Exception as e:
  86. return json.dumps(response)
  87. except Exception as e:
  88. return response.text
  89. else:
  90. raise ValueError(f'Invalid response type {type(response)}')
  91. def do_http_request(self, url: str, method: str, headers: dict[str, Any], parameters: dict[str, Any]) -> httpx.Response:
  92. """
  93. do http request depending on api bundle
  94. """
  95. method = method.lower()
  96. params = {}
  97. path_params = {}
  98. body = {}
  99. cookies = {}
  100. # check parameters
  101. for parameter in self.api_bundle.openapi.get('parameters', []):
  102. if parameter['in'] == 'path':
  103. value = ''
  104. if parameter['name'] in parameters:
  105. value = parameters[parameter['name']]
  106. elif parameter['required']:
  107. raise ToolProviderCredentialValidationError(f"Missing required parameter {parameter['name']}")
  108. else:
  109. value = (parameter.get('schema', {}) or {}).get('default', '')
  110. path_params[parameter['name']] = value
  111. elif parameter['in'] == 'query':
  112. value = ''
  113. if parameter['name'] in parameters:
  114. value = parameters[parameter['name']]
  115. elif parameter['required']:
  116. raise ToolProviderCredentialValidationError(f"Missing required parameter {parameter['name']}")
  117. else:
  118. value = (parameter.get('schema', {}) or {}).get('default', '')
  119. params[parameter['name']] = value
  120. elif parameter['in'] == 'cookie':
  121. value = ''
  122. if parameter['name'] in parameters:
  123. value = parameters[parameter['name']]
  124. elif parameter['required']:
  125. raise ToolProviderCredentialValidationError(f"Missing required parameter {parameter['name']}")
  126. else:
  127. value = (parameter.get('schema', {}) or {}).get('default', '')
  128. cookies[parameter['name']] = value
  129. elif parameter['in'] == 'header':
  130. value = ''
  131. if parameter['name'] in parameters:
  132. value = parameters[parameter['name']]
  133. elif parameter['required']:
  134. raise ToolProviderCredentialValidationError(f"Missing required parameter {parameter['name']}")
  135. else:
  136. value = (parameter.get('schema', {}) or {}).get('default', '')
  137. headers[parameter['name']] = value
  138. # check if there is a request body and handle it
  139. if 'requestBody' in self.api_bundle.openapi and self.api_bundle.openapi['requestBody'] is not None:
  140. # handle json request body
  141. if 'content' in self.api_bundle.openapi['requestBody']:
  142. for content_type in self.api_bundle.openapi['requestBody']['content']:
  143. headers['Content-Type'] = content_type
  144. body_schema = self.api_bundle.openapi['requestBody']['content'][content_type]['schema']
  145. required = body_schema['required'] if 'required' in body_schema else []
  146. properties = body_schema['properties'] if 'properties' in body_schema else {}
  147. for name, property in properties.items():
  148. if name in parameters:
  149. # convert type
  150. try:
  151. value = parameters[name]
  152. if property['type'] == 'integer':
  153. value = int(value)
  154. elif property['type'] == 'number':
  155. # check if it is a float
  156. if '.' in value:
  157. value = float(value)
  158. else:
  159. value = int(value)
  160. elif property['type'] == 'boolean':
  161. value = bool(value)
  162. body[name] = value
  163. except ValueError as e:
  164. body[name] = parameters[name]
  165. elif name in required:
  166. raise ToolProviderCredentialValidationError(
  167. f"Missing required parameter {name} in operation {self.api_bundle.operation_id}"
  168. )
  169. elif 'default' in property:
  170. body[name] = property['default']
  171. else:
  172. body[name] = None
  173. break
  174. # replace path parameters
  175. for name, value in path_params.items():
  176. url = url.replace(f'{{{name}}}', f'{value}')
  177. # parse http body data if needed, for GET/HEAD/OPTIONS/TRACE, the body is ignored
  178. if 'Content-Type' in headers:
  179. if headers['Content-Type'] == 'application/json':
  180. body = dumps(body)
  181. else:
  182. body = body
  183. # do http request
  184. if method == 'get':
  185. response = ssrf_proxy.get(url, params=params, headers=headers, cookies=cookies, timeout=10, follow_redirects=True)
  186. elif method == 'post':
  187. response = ssrf_proxy.post(url, params=params, headers=headers, cookies=cookies, data=body, timeout=10, follow_redirects=True)
  188. elif method == 'put':
  189. response = ssrf_proxy.put(url, params=params, headers=headers, cookies=cookies, data=body, timeout=10, follow_redirects=True)
  190. elif method == 'delete':
  191. """
  192. request body data is unsupported for DELETE method in standard http protocol
  193. however, OpenAPI 3.0 supports request body data for DELETE method, so we support it here by using requests
  194. """
  195. response = ssrf_proxy.delete(url, params=params, headers=headers, cookies=cookies, data=body, timeout=10, allow_redirects=True)
  196. elif method == 'patch':
  197. response = ssrf_proxy.patch(url, params=params, headers=headers, cookies=cookies, data=body, timeout=10, follow_redirects=True)
  198. elif method == 'head':
  199. response = ssrf_proxy.head(url, params=params, headers=headers, cookies=cookies, timeout=10, follow_redirects=True)
  200. elif method == 'options':
  201. response = ssrf_proxy.options(url, params=params, headers=headers, cookies=cookies, timeout=10, follow_redirects=True)
  202. else:
  203. raise ValueError(f'Invalid http method {method}')
  204. return response
  205. def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage | list[ToolInvokeMessage]:
  206. """
  207. invoke http request
  208. """
  209. # assemble request
  210. headers = self.assembling_request(tool_parameters)
  211. # do http request
  212. response = self.do_http_request(self.api_bundle.server_url, self.api_bundle.method, headers, tool_parameters)
  213. # validate response
  214. response = self.validate_and_parse_response(response)
  215. # assemble invoke message
  216. return self.create_text_message(response)