api_tool.py 11 KB

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