api_tool.py 10 KB

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