parser.py 15 KB

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