parser.py 15 KB

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