mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-04-22 05:39:42 +08:00

Signed-off-by: yihong0618 <zouzou0208@gmail.com> Signed-off-by: -LAN- <laipz8200@outlook.com> Signed-off-by: xhe <xw897002528@gmail.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: takatost <takatost@gmail.com> Co-authored-by: kurokobo <kuro664@gmail.com> Co-authored-by: Novice Lee <novicelee@NoviPro.local> Co-authored-by: zxhlyh <jasonapring2015@outlook.com> Co-authored-by: AkaraChen <akarachen@outlook.com> Co-authored-by: Yi <yxiaoisme@gmail.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: twwu <twwu@dify.ai> Co-authored-by: Hiroshi Fujita <fujita-h@users.noreply.github.com> Co-authored-by: AkaraChen <85140972+AkaraChen@users.noreply.github.com> Co-authored-by: NFish <douxc512@gmail.com> Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: Novice <857526207@qq.com> Co-authored-by: Hiroki Nagai <82458324+nagaihiroki-git@users.noreply.github.com> Co-authored-by: Gen Sato <52241300+halogen22@users.noreply.github.com> Co-authored-by: eux <euxuuu@gmail.com> Co-authored-by: huangzhuo1949 <167434202+huangzhuo1949@users.noreply.github.com> Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com> Co-authored-by: lotsik <lotsik@mail.ru> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: gakkiyomi <gakkiyomi@aliyun.com> Co-authored-by: CN-P5 <heibai2006@gmail.com> Co-authored-by: CN-P5 <heibai2006@qq.com> Co-authored-by: Chuehnone <1897025+chuehnone@users.noreply.github.com> Co-authored-by: yihong <zouzou0208@gmail.com> Co-authored-by: Kevin9703 <51311316+Kevin9703@users.noreply.github.com> Co-authored-by: -LAN- <laipz8200@outlook.com> Co-authored-by: Boris Feld <lothiraldan@gmail.com> Co-authored-by: mbo <himabo@gmail.com> Co-authored-by: mabo <mabo@aeyes.ai> Co-authored-by: Warren Chen <warren.chen830@gmail.com> Co-authored-by: JzoNgKVO <27049666+JzoNgKVO@users.noreply.github.com> Co-authored-by: jiandanfeng <chenjh3@wangsu.com> Co-authored-by: zhu-an <70234959+xhdd123321@users.noreply.github.com> Co-authored-by: zhaoqingyu.1075 <zhaoqingyu.1075@bytedance.com> Co-authored-by: 海狸大師 <86974027+yenslife@users.noreply.github.com> Co-authored-by: Xu Song <xusong.vip@gmail.com> Co-authored-by: rayshaw001 <396301947@163.com> Co-authored-by: Ding Jiatong <dingjiatong@gmail.com> Co-authored-by: Bowen Liang <liangbowen@gf.com.cn> Co-authored-by: JasonVV <jasonwangiii@outlook.com> Co-authored-by: le0zh <newlight@qq.com> Co-authored-by: zhuxinliang <zhuxinliang@didiglobal.com> Co-authored-by: k-zaku <zaku99@outlook.jp> Co-authored-by: luckylhb90 <luckylhb90@gmail.com> Co-authored-by: hobo.l <hobo.l@binance.com> Co-authored-by: jiangbo721 <365065261@qq.com> Co-authored-by: 刘江波 <jiangbo721@163.com> Co-authored-by: Shun Miyazawa <34241526+miya@users.noreply.github.com> Co-authored-by: EricPan <30651140+Egfly@users.noreply.github.com> Co-authored-by: crazywoola <427733928@qq.com> Co-authored-by: sino <sino2322@gmail.com> Co-authored-by: Jhvcc <37662342+Jhvcc@users.noreply.github.com> Co-authored-by: lowell <lowell.hu@zkteco.in> Co-authored-by: Boris Polonsky <BorisPolonsky@users.noreply.github.com> Co-authored-by: Ademílson Tonato <ademilsonft@outlook.com> Co-authored-by: Ademílson Tonato <ademilson.tonato@refurbed.com> Co-authored-by: IWAI, Masaharu <iwaim.sub@gmail.com> Co-authored-by: Yueh-Po Peng (Yabi) <94939112+y10ab1@users.noreply.github.com> Co-authored-by: Jason <ggbbddjm@gmail.com> Co-authored-by: Xin Zhang <sjhpzx@gmail.com> Co-authored-by: yjc980121 <3898524+yjc980121@users.noreply.github.com> Co-authored-by: heyszt <36215648+hieheihei@users.noreply.github.com> Co-authored-by: Abdullah AlOsaimi <osaimiacc@gmail.com> Co-authored-by: Abdullah AlOsaimi <189027247+osaimi@users.noreply.github.com> Co-authored-by: Yingchun Lai <laiyingchun@apache.org> Co-authored-by: Hash Brown <hi@xzd.me> Co-authored-by: zuodongxu <192560071+zuodongxu@users.noreply.github.com> Co-authored-by: Masashi Tomooka <tmokmss@users.noreply.github.com> Co-authored-by: aplio <ryo.091219@gmail.com> Co-authored-by: Obada Khalili <54270856+obadakhalili@users.noreply.github.com> Co-authored-by: Nam Vu <zuzoovn@gmail.com> Co-authored-by: Kei YAMAZAKI <1715090+kei-yamazaki@users.noreply.github.com> Co-authored-by: TechnoHouse <13776377+deephbz@users.noreply.github.com> Co-authored-by: Riddhimaan-Senapati <114703025+Riddhimaan-Senapati@users.noreply.github.com> Co-authored-by: MaFee921 <31881301+2284730142@users.noreply.github.com> Co-authored-by: te-chan <t-nakanome@sakura-is.co.jp> Co-authored-by: HQidea <HQidea@users.noreply.github.com> Co-authored-by: Joshbly <36315710+Joshbly@users.noreply.github.com> Co-authored-by: xhe <xw897002528@gmail.com> Co-authored-by: weiwenyan-dev <154779315+weiwenyan-dev@users.noreply.github.com> Co-authored-by: ex_wenyan.wei <ex_wenyan.wei@tcl.com> Co-authored-by: engchina <12236799+engchina@users.noreply.github.com> Co-authored-by: engchina <atjapan2015@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: 呆萌闷油瓶 <253605712@qq.com> Co-authored-by: Kemal <kemalmeler@outlook.com> Co-authored-by: Lazy_Frog <4590648+lazyFrogLOL@users.noreply.github.com> Co-authored-by: Yi Xiao <54782454+YIXIAO0@users.noreply.github.com> Co-authored-by: Steven sun <98230804+Tuyohai@users.noreply.github.com> Co-authored-by: steven <sunzwj@digitalchina.com> Co-authored-by: Kalo Chin <91766386+fdb02983rhy@users.noreply.github.com> Co-authored-by: Katy Tao <34019945+KatyTao@users.noreply.github.com> Co-authored-by: depy <42985524+h4ckdepy@users.noreply.github.com> Co-authored-by: 胡春东 <gycm520@gmail.com> Co-authored-by: Junjie.M <118170653@qq.com> Co-authored-by: MuYu <mr.muzea@gmail.com> Co-authored-by: Naoki Takashima <39912547+takatea@users.noreply.github.com> Co-authored-by: Summer-Gu <37869445+gubinjie@users.noreply.github.com> Co-authored-by: Fei He <droxer.he@gmail.com> Co-authored-by: ybalbert001 <120714773+ybalbert001@users.noreply.github.com> Co-authored-by: Yuanbo Li <ybalbert@amazon.com> Co-authored-by: douxc <7553076+douxc@users.noreply.github.com> Co-authored-by: liuzhenghua <1090179900@qq.com> Co-authored-by: Wu Jiayang <62842862+Wu-Jiayang@users.noreply.github.com> Co-authored-by: Your Name <you@example.com> Co-authored-by: kimjion <45935338+kimjion@users.noreply.github.com> Co-authored-by: AugNSo <song.tiankai@icloud.com> Co-authored-by: llinvokerl <38915183+llinvokerl@users.noreply.github.com> Co-authored-by: liusurong.lsr <liusurong.lsr@alibaba-inc.com> Co-authored-by: Vasu Negi <vasu-negi@users.noreply.github.com> Co-authored-by: Hundredwz <1808096180@qq.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
381 lines
16 KiB
Python
381 lines
16 KiB
Python
import re
|
|
import uuid
|
|
from json import dumps as json_dumps
|
|
from json import loads as json_loads
|
|
from json.decoder import JSONDecodeError
|
|
from typing import Optional
|
|
|
|
from flask import request
|
|
from requests import get
|
|
from yaml import YAMLError, safe_load # type: ignore
|
|
|
|
from core.tools.entities.common_entities import I18nObject
|
|
from core.tools.entities.tool_bundle import ApiToolBundle
|
|
from core.tools.entities.tool_entities import ApiProviderSchemaType, ToolParameter
|
|
from core.tools.errors import ToolApiSchemaError, ToolNotSupportedError, ToolProviderNotFoundError
|
|
|
|
|
|
class ApiBasedToolSchemaParser:
|
|
@staticmethod
|
|
def parse_openapi_to_tool_bundle(
|
|
openapi: dict, extra_info: dict | None = None, warning: dict | None = None
|
|
) -> list[ApiToolBundle]:
|
|
warning = warning if warning is not None else {}
|
|
extra_info = extra_info if extra_info is not None else {}
|
|
|
|
# set description to extra_info
|
|
extra_info["description"] = openapi["info"].get("description", "")
|
|
|
|
if len(openapi["servers"]) == 0:
|
|
raise ToolProviderNotFoundError("No server found in the openapi yaml.")
|
|
|
|
server_url = openapi["servers"][0]["url"]
|
|
request_env = request.headers.get("X-Request-Env")
|
|
if request_env:
|
|
matched_servers = [server["url"] for server in openapi["servers"] if server["env"] == request_env]
|
|
server_url = matched_servers[0] if matched_servers else server_url
|
|
|
|
# list all interfaces
|
|
interfaces = []
|
|
for path, path_item in openapi["paths"].items():
|
|
methods = ["get", "post", "put", "delete", "patch", "head", "options", "trace"]
|
|
for method in methods:
|
|
if method in path_item:
|
|
interfaces.append(
|
|
{
|
|
"path": path,
|
|
"method": method,
|
|
"operation": path_item[method],
|
|
}
|
|
)
|
|
|
|
# get all parameters
|
|
bundles = []
|
|
for interface in interfaces:
|
|
# convert parameters
|
|
parameters = []
|
|
if "parameters" in interface["operation"]:
|
|
for parameter in interface["operation"]["parameters"]:
|
|
tool_parameter = ToolParameter(
|
|
name=parameter["name"],
|
|
label=I18nObject(en_US=parameter["name"], zh_Hans=parameter["name"]),
|
|
human_description=I18nObject(
|
|
en_US=parameter.get("description", ""), zh_Hans=parameter.get("description", "")
|
|
),
|
|
type=ToolParameter.ToolParameterType.STRING,
|
|
required=parameter.get("required", False),
|
|
form=ToolParameter.ToolParameterForm.LLM,
|
|
llm_description=parameter.get("description"),
|
|
default=parameter["schema"]["default"]
|
|
if "schema" in parameter and "default" in parameter["schema"]
|
|
else None,
|
|
placeholder=I18nObject(
|
|
en_US=parameter.get("description", ""), zh_Hans=parameter.get("description", "")
|
|
),
|
|
)
|
|
|
|
# check if there is a type
|
|
typ = ApiBasedToolSchemaParser._get_tool_parameter_type(parameter)
|
|
if typ:
|
|
tool_parameter.type = typ
|
|
|
|
parameters.append(tool_parameter)
|
|
# create tool bundle
|
|
# check if there is a request body
|
|
if "requestBody" in interface["operation"]:
|
|
request_body = interface["operation"]["requestBody"]
|
|
if "content" in request_body:
|
|
for content_type, content in request_body["content"].items():
|
|
# if there is a reference, get the reference and overwrite the content
|
|
if "schema" not in content:
|
|
continue
|
|
|
|
if "$ref" in content["schema"]:
|
|
# get the reference
|
|
root = openapi
|
|
reference = content["schema"]["$ref"].split("/")[1:]
|
|
for ref in reference:
|
|
root = root[ref]
|
|
# overwrite the content
|
|
interface["operation"]["requestBody"]["content"][content_type]["schema"] = root
|
|
|
|
# parse body parameters
|
|
if "schema" in interface["operation"]["requestBody"]["content"][content_type]:
|
|
body_schema = interface["operation"]["requestBody"]["content"][content_type]["schema"]
|
|
required = body_schema.get("required", [])
|
|
properties = body_schema.get("properties", {})
|
|
for name, property in properties.items():
|
|
tool = ToolParameter(
|
|
name=name,
|
|
label=I18nObject(en_US=name, zh_Hans=name),
|
|
human_description=I18nObject(
|
|
en_US=property.get("description", ""), zh_Hans=property.get("description", "")
|
|
),
|
|
type=ToolParameter.ToolParameterType.STRING,
|
|
required=name in required,
|
|
form=ToolParameter.ToolParameterForm.LLM,
|
|
llm_description=property.get("description", ""),
|
|
default=property.get("default", None),
|
|
placeholder=I18nObject(
|
|
en_US=property.get("description", ""), zh_Hans=property.get("description", "")
|
|
),
|
|
)
|
|
|
|
# check if there is a type
|
|
typ = ApiBasedToolSchemaParser._get_tool_parameter_type(property)
|
|
if typ:
|
|
tool.type = typ
|
|
|
|
parameters.append(tool)
|
|
|
|
# check if parameters is duplicated
|
|
parameters_count = {}
|
|
for parameter in parameters:
|
|
if parameter.name not in parameters_count:
|
|
parameters_count[parameter.name] = 0
|
|
parameters_count[parameter.name] += 1
|
|
for name, count in parameters_count.items():
|
|
if count > 1:
|
|
warning["duplicated_parameter"] = f"Parameter {name} is duplicated."
|
|
|
|
# check if there is a operation id, use $path_$method as operation id if not
|
|
if "operationId" not in interface["operation"]:
|
|
# remove special characters like / to ensure the operation id is valid ^[a-zA-Z0-9_-]{1,64}$
|
|
path = interface["path"]
|
|
if interface["path"].startswith("/"):
|
|
path = interface["path"][1:]
|
|
# remove special characters like / to ensure the operation id is valid ^[a-zA-Z0-9_-]{1,64}$
|
|
path = re.sub(r"[^a-zA-Z0-9_-]", "", path)
|
|
if not path:
|
|
path = str(uuid.uuid4())
|
|
|
|
interface["operation"]["operationId"] = f"{path}_{interface['method']}"
|
|
|
|
bundles.append(
|
|
ApiToolBundle(
|
|
server_url=server_url + interface["path"],
|
|
method=interface["method"],
|
|
summary=interface["operation"]["description"]
|
|
if "description" in interface["operation"]
|
|
else interface["operation"].get("summary", None),
|
|
operation_id=interface["operation"]["operationId"],
|
|
parameters=parameters,
|
|
author="",
|
|
icon=None,
|
|
openapi=interface["operation"],
|
|
)
|
|
)
|
|
|
|
return bundles
|
|
|
|
@staticmethod
|
|
def _get_tool_parameter_type(parameter: dict) -> Optional[ToolParameter.ToolParameterType]:
|
|
parameter = parameter or {}
|
|
typ: Optional[str] = None
|
|
if parameter.get("format") == "binary":
|
|
return ToolParameter.ToolParameterType.FILE
|
|
|
|
if "type" in parameter:
|
|
typ = parameter["type"]
|
|
elif "schema" in parameter and "type" in parameter["schema"]:
|
|
typ = parameter["schema"]["type"]
|
|
|
|
if typ in {"integer", "number"}:
|
|
return ToolParameter.ToolParameterType.NUMBER
|
|
elif typ == "boolean":
|
|
return ToolParameter.ToolParameterType.BOOLEAN
|
|
elif typ == "string":
|
|
return ToolParameter.ToolParameterType.STRING
|
|
else:
|
|
return None
|
|
|
|
@staticmethod
|
|
def parse_openapi_yaml_to_tool_bundle(
|
|
yaml: str, extra_info: dict | None = None, warning: dict | None = None
|
|
) -> list[ApiToolBundle]:
|
|
"""
|
|
parse openapi yaml to tool bundle
|
|
|
|
:param yaml: the yaml string
|
|
:return: the tool bundle
|
|
"""
|
|
warning = warning if warning is not None else {}
|
|
extra_info = extra_info if extra_info is not None else {}
|
|
|
|
openapi: dict = safe_load(yaml)
|
|
if openapi is None:
|
|
raise ToolApiSchemaError("Invalid openapi yaml.")
|
|
return ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(openapi, extra_info=extra_info, warning=warning)
|
|
|
|
@staticmethod
|
|
def parse_swagger_to_openapi(swagger: dict, extra_info: dict | None = None, warning: dict | None = None) -> dict:
|
|
warning = warning or {}
|
|
"""
|
|
parse swagger to openapi
|
|
|
|
:param swagger: the swagger dict
|
|
:return: the openapi dict
|
|
"""
|
|
# convert swagger to openapi
|
|
info = swagger.get("info", {"title": "Swagger", "description": "Swagger", "version": "1.0.0"})
|
|
|
|
servers = swagger.get("servers", [])
|
|
|
|
if len(servers) == 0:
|
|
raise ToolApiSchemaError("No server found in the swagger yaml.")
|
|
|
|
openapi = {
|
|
"openapi": "3.0.0",
|
|
"info": {
|
|
"title": info.get("title", "Swagger"),
|
|
"description": info.get("description", "Swagger"),
|
|
"version": info.get("version", "1.0.0"),
|
|
},
|
|
"servers": swagger["servers"],
|
|
"paths": {},
|
|
"components": {"schemas": {}},
|
|
}
|
|
|
|
# check paths
|
|
if "paths" not in swagger or len(swagger["paths"]) == 0:
|
|
raise ToolApiSchemaError("No paths found in the swagger yaml.")
|
|
|
|
# convert paths
|
|
for path, path_item in swagger["paths"].items():
|
|
openapi["paths"][path] = {}
|
|
for method, operation in path_item.items():
|
|
if "operationId" not in operation:
|
|
raise ToolApiSchemaError(f"No operationId found in operation {method} {path}.")
|
|
|
|
if ("summary" not in operation or len(operation["summary"]) == 0) and (
|
|
"description" not in operation or len(operation["description"]) == 0
|
|
):
|
|
if warning is not None:
|
|
warning["missing_summary"] = f"No summary or description found in operation {method} {path}."
|
|
|
|
openapi["paths"][path][method] = {
|
|
"operationId": operation["operationId"],
|
|
"summary": operation.get("summary", ""),
|
|
"description": operation.get("description", ""),
|
|
"parameters": operation.get("parameters", []),
|
|
"responses": operation.get("responses", {}),
|
|
}
|
|
|
|
if "requestBody" in operation:
|
|
openapi["paths"][path][method]["requestBody"] = operation["requestBody"]
|
|
|
|
# convert definitions
|
|
for name, definition in swagger["definitions"].items():
|
|
openapi["components"]["schemas"][name] = definition
|
|
|
|
return openapi
|
|
|
|
@staticmethod
|
|
def parse_openai_plugin_json_to_tool_bundle(
|
|
json: str, extra_info: dict | None = None, warning: dict | None = None
|
|
) -> list[ApiToolBundle]:
|
|
"""
|
|
parse openapi plugin yaml to tool bundle
|
|
|
|
:param json: the json string
|
|
:return: the tool bundle
|
|
"""
|
|
warning = warning if warning is not None else {}
|
|
extra_info = extra_info if extra_info is not None else {}
|
|
|
|
try:
|
|
openai_plugin = json_loads(json)
|
|
api = openai_plugin["api"]
|
|
api_url = api["url"]
|
|
api_type = api["type"]
|
|
except JSONDecodeError:
|
|
raise ToolProviderNotFoundError("Invalid openai plugin json.")
|
|
|
|
if api_type != "openapi":
|
|
raise ToolNotSupportedError("Only openapi is supported now.")
|
|
|
|
# get openapi yaml
|
|
response = get(api_url, headers={"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "}, timeout=5)
|
|
|
|
if response.status_code != 200:
|
|
raise ToolProviderNotFoundError("cannot get openapi yaml from url.")
|
|
|
|
return ApiBasedToolSchemaParser.parse_openapi_yaml_to_tool_bundle(
|
|
response.text, extra_info=extra_info, warning=warning
|
|
)
|
|
|
|
@staticmethod
|
|
def auto_parse_to_tool_bundle(
|
|
content: str, extra_info: dict | None = None, warning: dict | None = None
|
|
) -> tuple[list[ApiToolBundle], str]:
|
|
"""
|
|
auto parse to tool bundle
|
|
|
|
:param content: the content
|
|
:return: tools bundle, schema_type
|
|
"""
|
|
warning = warning if warning is not None else {}
|
|
extra_info = extra_info if extra_info is not None else {}
|
|
|
|
content = content.strip()
|
|
loaded_content = None
|
|
json_error = None
|
|
yaml_error = None
|
|
|
|
try:
|
|
loaded_content = json_loads(content)
|
|
except JSONDecodeError as e:
|
|
json_error = e
|
|
|
|
if loaded_content is None:
|
|
try:
|
|
loaded_content = safe_load(content)
|
|
except YAMLError as e:
|
|
yaml_error = e
|
|
if loaded_content is None:
|
|
raise ToolApiSchemaError(
|
|
f"Invalid api schema, schema is neither json nor yaml. json error: {str(json_error)},"
|
|
f" yaml error: {str(yaml_error)}"
|
|
)
|
|
|
|
swagger_error = None
|
|
openapi_error = None
|
|
openapi_plugin_error = None
|
|
schema_type = None
|
|
|
|
try:
|
|
openapi = ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(
|
|
loaded_content, extra_info=extra_info, warning=warning
|
|
)
|
|
schema_type = ApiProviderSchemaType.OPENAPI.value
|
|
return openapi, schema_type
|
|
except ToolApiSchemaError as e:
|
|
openapi_error = e
|
|
|
|
# openai parse error, fallback to swagger
|
|
try:
|
|
converted_swagger = ApiBasedToolSchemaParser.parse_swagger_to_openapi(
|
|
loaded_content, extra_info=extra_info, warning=warning
|
|
)
|
|
schema_type = ApiProviderSchemaType.SWAGGER.value
|
|
return ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(
|
|
converted_swagger, extra_info=extra_info, warning=warning
|
|
), schema_type
|
|
except ToolApiSchemaError as e:
|
|
swagger_error = e
|
|
|
|
# swagger parse error, fallback to openai plugin
|
|
try:
|
|
openapi_plugin = ApiBasedToolSchemaParser.parse_openai_plugin_json_to_tool_bundle(
|
|
json_dumps(loaded_content), extra_info=extra_info, warning=warning
|
|
)
|
|
return openapi_plugin, ApiProviderSchemaType.OPENAI_PLUGIN.value
|
|
except ToolNotSupportedError as e:
|
|
# maybe it's not plugin at all
|
|
openapi_plugin_error = e
|
|
|
|
raise ToolApiSchemaError(
|
|
f"Invalid api schema, openapi error: {str(openapi_error)}, swagger error: {str(swagger_error)},"
|
|
f" openapi plugin error: {str(openapi_plugin_error)}"
|
|
)
|