parser.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. from core.tools.entities.tool_bundle import ApiBasedToolBundle
  2. from core.tools.entities.tool_entities import ToolParamter, ToolParamterOption, ApiProviderSchemaType
  3. from core.tools.entities.common_entities import I18nObject
  4. from core.tools.errors import ToolProviderNotFoundError, ToolNotSupportedError, \
  5. ToolApiSchemaError
  6. from typing import List, Tuple
  7. from yaml import FullLoader, load
  8. from json import loads as json_loads, dumps as json_dumps
  9. from requests import get
  10. class ApiBasedToolSchemaParser:
  11. @staticmethod
  12. def parse_openapi_to_tool_bundle(openapi: dict, extra_info: dict = None, warning: dict = None) -> List[ApiBasedToolBundle]:
  13. warning = warning if warning is not None else {}
  14. extra_info = extra_info if extra_info is not None else {}
  15. # set description to extra_info
  16. if 'description' in openapi['info']:
  17. extra_info['description'] = openapi['info']['description']
  18. else:
  19. extra_info['description'] = ''
  20. if len(openapi['servers']) == 0:
  21. raise ToolProviderNotFoundError('No server found in the openapi yaml.')
  22. server_url = openapi['servers'][0]['url']
  23. # list all interfaces
  24. interfaces = []
  25. for path, path_item in openapi['paths'].items():
  26. methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace']
  27. for method in methods:
  28. if method in path_item:
  29. interfaces.append({
  30. 'path': path,
  31. 'method': method,
  32. 'operation': path_item[method],
  33. })
  34. # get all parameters
  35. bundles = []
  36. for interface in interfaces:
  37. # convert parameters
  38. parameters = []
  39. if 'parameters' in interface['operation']:
  40. for parameter in interface['operation']['parameters']:
  41. parameters.append(ToolParamter(
  42. name=parameter['name'],
  43. label=I18nObject(
  44. en_US=parameter['name'],
  45. zh_Hans=parameter['name']
  46. ),
  47. human_description=I18nObject(
  48. en_US=parameter.get('description', ''),
  49. zh_Hans=parameter.get('description', '')
  50. ),
  51. type=ToolParamter.ToolParameterType.STRING,
  52. required=parameter.get('required', False),
  53. form=ToolParamter.ToolParameterForm.LLM,
  54. llm_description=parameter.get('description'),
  55. default=parameter['default'] if 'default' in parameter else None,
  56. ))
  57. # create tool bundle
  58. # check if there is a request body
  59. if 'requestBody' in interface['operation']:
  60. request_body = interface['operation']['requestBody']
  61. if 'content' in request_body:
  62. for content_type, content in request_body['content'].items():
  63. # if there is a reference, get the reference and overwrite the content
  64. if 'schema' not in content:
  65. content
  66. if '$ref' in content['schema']:
  67. # get the reference
  68. root = openapi
  69. reference = content['schema']['$ref'].split('/')[1:]
  70. for ref in reference:
  71. root = root[ref]
  72. # overwrite the content
  73. interface['operation']['requestBody']['content'][content_type]['schema'] = root
  74. # parse body parameters
  75. if 'schema' in interface['operation']['requestBody']['content'][content_type]:
  76. body_schema = interface['operation']['requestBody']['content'][content_type]['schema']
  77. required = body_schema['required'] if 'required' in body_schema else []
  78. properties = body_schema['properties'] if 'properties' in body_schema else {}
  79. for name, property in properties.items():
  80. parameters.append(ToolParamter(
  81. name=name,
  82. label=I18nObject(
  83. en_US=name,
  84. zh_Hans=name
  85. ),
  86. human_description=I18nObject(
  87. en_US=property['description'] if 'description' in property else '',
  88. zh_Hans=property['description'] if 'description' in property else ''
  89. ),
  90. type=ToolParamter.ToolParameterType.STRING,
  91. required=name in required,
  92. form=ToolParamter.ToolParameterForm.LLM,
  93. llm_description=property['description'] if 'description' in property else '',
  94. default=property['default'] if 'default' in property else None,
  95. ))
  96. # check if parameters is duplicated
  97. parameters_count = {}
  98. for parameter in parameters:
  99. if parameter.name not in parameters_count:
  100. parameters_count[parameter.name] = 0
  101. parameters_count[parameter.name] += 1
  102. for name, count in parameters_count.items():
  103. if count > 1:
  104. warning['duplicated_parameter'] = f'Parameter {name} is duplicated.'
  105. bundles.append(ApiBasedToolBundle(
  106. server_url=server_url + interface['path'],
  107. method=interface['method'],
  108. summary=interface['operation']['summary'] if 'summary' in interface['operation'] else None,
  109. operation_id=interface['operation']['operationId'],
  110. parameters=parameters,
  111. author='',
  112. icon=None,
  113. openapi=interface['operation'],
  114. ))
  115. return bundles
  116. @staticmethod
  117. def parse_openapi_yaml_to_tool_bundle(yaml: str, extra_info: dict = None, warning: dict = None) -> List[ApiBasedToolBundle]:
  118. """
  119. parse openapi yaml to tool bundle
  120. :param yaml: the yaml string
  121. :return: the tool bundle
  122. """
  123. warning = warning if warning is not None else {}
  124. extra_info = extra_info if extra_info is not None else {}
  125. openapi: dict = load(yaml, Loader=FullLoader)
  126. if openapi is None:
  127. raise ToolApiSchemaError('Invalid openapi yaml.')
  128. return ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(openapi, extra_info=extra_info, warning=warning)
  129. @staticmethod
  130. def parse_openapi_json_to_tool_bundle(json: str, extra_info: dict = None, warning: dict = None) -> List[ApiBasedToolBundle]:
  131. """
  132. parse openapi yaml to tool bundle
  133. :param yaml: the yaml string
  134. :return: the tool bundle
  135. """
  136. warning = warning if warning is not None else {}
  137. extra_info = extra_info if extra_info is not None else {}
  138. openapi: dict = json_loads(json)
  139. if openapi is None:
  140. raise ToolApiSchemaError('Invalid openapi json.')
  141. return ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(openapi, extra_info=extra_info, warning=warning)
  142. @staticmethod
  143. def parse_swagger_to_openapi(swagger: dict, extra_info: dict = None, warning: dict = None) -> dict:
  144. """
  145. parse swagger to openapi
  146. :param swagger: the swagger dict
  147. :return: the openapi dict
  148. """
  149. # convert swagger to openapi
  150. info = swagger.get('info', {
  151. 'title': 'Swagger',
  152. 'description': 'Swagger',
  153. 'version': '1.0.0'
  154. })
  155. servers = swagger.get('servers', [])
  156. if len(servers) == 0:
  157. raise ToolApiSchemaError('No server found in the swagger yaml.')
  158. openapi = {
  159. 'openapi': '3.0.0',
  160. 'info': {
  161. 'title': info.get('title', 'Swagger'),
  162. 'description': info.get('description', 'Swagger'),
  163. 'version': info.get('version', '1.0.0')
  164. },
  165. 'servers': swagger['servers'],
  166. 'paths': {},
  167. 'components': {
  168. 'schemas': {}
  169. }
  170. }
  171. # check paths
  172. if 'paths' not in swagger or len(swagger['paths']) == 0:
  173. raise ToolApiSchemaError('No paths found in the swagger yaml.')
  174. # convert paths
  175. for path, path_item in swagger['paths'].items():
  176. openapi['paths'][path] = {}
  177. for method, operation in path_item.items():
  178. if 'operationId' not in operation:
  179. raise ToolApiSchemaError(f'No operationId found in operation {method} {path}.')
  180. if 'summary' not in operation or len(operation['summary']) == 0:
  181. warning['missing_summary'] = f'No summary found in operation {method} {path}.'
  182. if 'description' not in operation or len(operation['description']) == 0:
  183. warning['missing_description'] = f'No description found in operation {method} {path}.'
  184. openapi['paths'][path][method] = {
  185. 'operationId': operation['operationId'],
  186. 'summary': operation.get('summary', ''),
  187. 'description': operation.get('description', ''),
  188. 'parameters': operation.get('parameters', []),
  189. 'responses': operation.get('responses', {}),
  190. }
  191. if 'requestBody' in operation:
  192. openapi['paths'][path][method]['requestBody'] = operation['requestBody']
  193. # convert definitions
  194. for name, definition in swagger['definitions'].items():
  195. openapi['components']['schemas'][name] = definition
  196. return openapi
  197. @staticmethod
  198. def parse_swagger_yaml_to_tool_bundle(yaml: str, extra_info: dict = None, warning: dict = None) -> List[ApiBasedToolBundle]:
  199. """
  200. parse swagger yaml to tool bundle
  201. :param yaml: the yaml string
  202. :return: the tool bundle
  203. """
  204. warning = warning if warning is not None else {}
  205. extra_info = extra_info if extra_info is not None else {}
  206. swagger: dict = load(yaml, Loader=FullLoader)
  207. openapi = ApiBasedToolSchemaParser.parse_swagger_to_openapi(swagger, extra_info=extra_info, warning=warning)
  208. return ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(openapi, extra_info=extra_info, warning=warning)
  209. @staticmethod
  210. def parse_swagger_json_to_tool_bundle(json: str, extra_info: dict = None, warning: dict = None) -> List[ApiBasedToolBundle]:
  211. """
  212. parse swagger yaml to tool bundle
  213. :param yaml: the yaml string
  214. :return: the tool bundle
  215. """
  216. warning = warning if warning is not None else {}
  217. extra_info = extra_info if extra_info is not None else {}
  218. swagger: dict = json_loads(json)
  219. openapi = ApiBasedToolSchemaParser.parse_swagger_to_openapi(swagger, extra_info=extra_info, warning=warning)
  220. return ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(openapi, extra_info=extra_info, warning=warning)
  221. @staticmethod
  222. def parse_openai_plugin_json_to_tool_bundle(json: str, extra_info: dict = None, warning: dict = None) -> List[ApiBasedToolBundle]:
  223. """
  224. parse openapi plugin yaml to tool bundle
  225. :param json: the json string
  226. :return: the tool bundle
  227. """
  228. warning = warning if warning is not None else {}
  229. extra_info = extra_info if extra_info is not None else {}
  230. try:
  231. openai_plugin = json_loads(json)
  232. api = openai_plugin['api']
  233. api_url = api['url']
  234. api_type = api['type']
  235. except:
  236. raise ToolProviderNotFoundError('Invalid openai plugin json.')
  237. if api_type != 'openapi':
  238. raise ToolNotSupportedError('Only openapi is supported now.')
  239. # get openapi yaml
  240. response = get(api_url, headers={
  241. 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '
  242. }, timeout=5)
  243. if response.status_code != 200:
  244. raise ToolProviderNotFoundError('cannot get openapi yaml from url.')
  245. return ApiBasedToolSchemaParser.parse_openapi_yaml_to_tool_bundle(response.text, extra_info=extra_info, warning=warning)
  246. @staticmethod
  247. def auto_parse_to_tool_bundle(content: str, extra_info: dict = None, warning: dict = None) -> Tuple[List[ApiBasedToolBundle], str]:
  248. """
  249. auto parse to tool bundle
  250. :param content: the content
  251. :return: tools bundle, schema_type
  252. """
  253. warning = warning if warning is not None else {}
  254. extra_info = extra_info if extra_info is not None else {}
  255. json_possible = False
  256. content = content.strip()
  257. if content.startswith('{') and content.endswith('}'):
  258. json_possible = True
  259. if json_possible:
  260. try:
  261. return ApiBasedToolSchemaParser.parse_openapi_json_to_tool_bundle(content, extra_info=extra_info, warning=warning), \
  262. ApiProviderSchemaType.OPENAPI.value
  263. except:
  264. pass
  265. try:
  266. return ApiBasedToolSchemaParser.parse_swagger_json_to_tool_bundle(content, extra_info=extra_info, warning=warning), \
  267. ApiProviderSchemaType.SWAGGER.value
  268. except:
  269. pass
  270. try:
  271. return ApiBasedToolSchemaParser.parse_openai_plugin_json_to_tool_bundle(content, extra_info=extra_info, warning=warning), \
  272. ApiProviderSchemaType.OPENAI_PLUGIN.value
  273. except:
  274. pass
  275. else:
  276. try:
  277. return ApiBasedToolSchemaParser.parse_openapi_yaml_to_tool_bundle(content, extra_info=extra_info, warning=warning), \
  278. ApiProviderSchemaType.OPENAPI.value
  279. except:
  280. pass
  281. try:
  282. return ApiBasedToolSchemaParser.parse_swagger_yaml_to_tool_bundle(content, extra_info=extra_info, warning=warning), \
  283. ApiProviderSchemaType.SWAGGER.value
  284. except:
  285. pass
  286. raise ToolApiSchemaError('Invalid api schema.')