api_tool.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import json
  2. from json import dumps
  3. from typing import Any, Dict, List, 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. path_params[parameter['name']] = value
  109. elif parameter['in'] == 'query':
  110. value = ''
  111. if parameter['name'] in parameters:
  112. value = parameters[parameter['name']]
  113. elif parameter['required']:
  114. raise ToolProviderCredentialValidationError(f"Missing required parameter {parameter['name']}")
  115. params[parameter['name']] = value
  116. elif parameter['in'] == 'cookie':
  117. value = ''
  118. if parameter['name'] in parameters:
  119. value = parameters[parameter['name']]
  120. elif parameter['required']:
  121. raise ToolProviderCredentialValidationError(f"Missing required parameter {parameter['name']}")
  122. cookies[parameter['name']] = value
  123. elif parameter['in'] == 'header':
  124. value = ''
  125. if parameter['name'] in parameters:
  126. value = parameters[parameter['name']]
  127. elif parameter['required']:
  128. raise ToolProviderCredentialValidationError(f"Missing required parameter {parameter['name']}")
  129. headers[parameter['name']] = value
  130. # check if there is a request body and handle it
  131. if 'requestBody' in self.api_bundle.openapi and self.api_bundle.openapi['requestBody'] is not None:
  132. # handle json request body
  133. if 'content' in self.api_bundle.openapi['requestBody']:
  134. for content_type in self.api_bundle.openapi['requestBody']['content']:
  135. headers['Content-Type'] = content_type
  136. body_schema = self.api_bundle.openapi['requestBody']['content'][content_type]['schema']
  137. required = body_schema['required'] if 'required' in body_schema else []
  138. properties = body_schema['properties'] if 'properties' in body_schema else {}
  139. for name, property in properties.items():
  140. if name in parameters:
  141. # convert type
  142. try:
  143. value = parameters[name]
  144. if property['type'] == 'integer':
  145. value = int(value)
  146. elif property['type'] == 'number':
  147. # check if it is a float
  148. if '.' in value:
  149. value = float(value)
  150. else:
  151. value = int(value)
  152. elif property['type'] == 'boolean':
  153. value = bool(value)
  154. body[name] = value
  155. except ValueError as e:
  156. body[name] = parameters[name]
  157. elif name in required:
  158. raise ToolProviderCredentialValidationError(
  159. f"Missing required parameter {name} in operation {self.api_bundle.operation_id}"
  160. )
  161. elif 'default' in property:
  162. body[name] = property['default']
  163. else:
  164. body[name] = None
  165. break
  166. # replace path parameters
  167. for name, value in path_params.items():
  168. url = url.replace(f'{{{name}}}', value)
  169. # parse http body data if needed, for GET/HEAD/OPTIONS/TRACE, the body is ignored
  170. if 'Content-Type' in headers:
  171. if headers['Content-Type'] == 'application/json':
  172. body = dumps(body)
  173. else:
  174. body = body
  175. # do http request
  176. if method == 'get':
  177. response = ssrf_proxy.get(url, params=params, headers=headers, cookies=cookies, timeout=10, follow_redirects=True)
  178. elif method == 'post':
  179. response = ssrf_proxy.post(url, params=params, headers=headers, cookies=cookies, data=body, timeout=10, follow_redirects=True)
  180. elif method == 'put':
  181. response = ssrf_proxy.put(url, params=params, headers=headers, cookies=cookies, data=body, timeout=10, follow_redirects=True)
  182. elif method == 'delete':
  183. """
  184. request body data is unsupported for DELETE method in standard http protocol
  185. however, OpenAPI 3.0 supports request body data for DELETE method, so we support it here by using requests
  186. """
  187. response = ssrf_proxy.delete(url, params=params, headers=headers, cookies=cookies, data=body, timeout=10, allow_redirects=True)
  188. elif method == 'patch':
  189. response = ssrf_proxy.patch(url, params=params, headers=headers, cookies=cookies, data=body, timeout=10, follow_redirects=True)
  190. elif method == 'head':
  191. response = ssrf_proxy.head(url, params=params, headers=headers, cookies=cookies, timeout=10, follow_redirects=True)
  192. elif method == 'options':
  193. response = ssrf_proxy.options(url, params=params, headers=headers, cookies=cookies, timeout=10, follow_redirects=True)
  194. else:
  195. raise ValueError(f'Invalid http method {method}')
  196. return response
  197. def _invoke(self, user_id: str, tool_parameters: Dict[str, Any]) -> ToolInvokeMessage | List[ToolInvokeMessage]:
  198. """
  199. invoke http request
  200. """
  201. # assemble request
  202. headers = self.assembling_request(tool_parameters)
  203. # do http request
  204. response = self.do_http_request(self.api_bundle.server_url, self.api_bundle.method, headers, tool_parameters)
  205. # validate response
  206. response = self.validate_and_parse_response(response)
  207. # assemble invoke message
  208. return self.create_text_message(response)