Team Ai
Apppublic

Underground-Digital/Workflow-Engine

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
parser.py363 linesDownload Raw Back to utils
1import re2import uuid3from json import dumps as json_dumps4from json import loads as json_loads5from json.decoder import JSONDecodeError6from typing import Optional7 8from requests import get9from yaml import YAMLError, safe_load10 11from core.tools.entities.common_entities import I18nObject12from core.tools.entities.tool_bundle import ApiToolBundle13from core.tools.entities.tool_entities import ApiProviderSchemaType, ToolParameter14from core.tools.errors import ToolApiSchemaError, ToolNotSupportedError, ToolProviderNotFoundError15 16 17class ApiBasedToolSchemaParser:18    @staticmethod19    def parse_openapi_to_tool_bundle(20        openapi: dict, extra_info: Optional[dict], warning: Optional[dict]21    ) -> list[ApiToolBundle]:22        warning = warning if warning is not None else {}23        extra_info = extra_info if extra_info is not None else {}24 25        # set description to extra_info26        extra_info["description"] = openapi["info"].get("description", "")27 28        if len(openapi["servers"]) == 0:29            raise ToolProviderNotFoundError("No server found in the openapi yaml.")30 31        server_url = openapi["servers"][0]["url"]32 33        # list all interfaces34        interfaces = []35        for path, path_item in openapi["paths"].items():36            methods = ["get", "post", "put", "delete", "patch", "head", "options", "trace"]37            for method in methods:38                if method in path_item:39                    interfaces.append(40                        {41                            "path": path,42                            "method": method,43                            "operation": path_item[method],44                        }45                    )46 47        # get all parameters48        bundles = []49        for interface in interfaces:50            # convert parameters51            parameters = []52            if "parameters" in interface["operation"]:53                for parameter in interface["operation"]["parameters"]:54                    tool_parameter = ToolParameter(55                        name=parameter["name"],56                        label=I18nObject(en_US=parameter["name"], zh_Hans=parameter["name"]),57                        human_description=I18nObject(58                            en_US=parameter.get("description", ""), zh_Hans=parameter.get("description", "")59                        ),60                        type=ToolParameter.ToolParameterType.STRING,61                        required=parameter.get("required", False),62                        form=ToolParameter.ToolParameterForm.LLM,63                        llm_description=parameter.get("description"),64                        default=parameter["schema"]["default"]65                        if "schema" in parameter and "default" in parameter["schema"]66                        else None,67                    )68 69                    # check if there is a type70                    typ = ApiBasedToolSchemaParser._get_tool_parameter_type(parameter)71                    if typ:72                        tool_parameter.type = typ73 74                    parameters.append(tool_parameter)75            # create tool bundle76            # check if there is a request body77            if "requestBody" in interface["operation"]:78                request_body = interface["operation"]["requestBody"]79                if "content" in request_body:80                    for content_type, content in request_body["content"].items():81                        # if there is a reference, get the reference and overwrite the content82                        if "schema" not in content:83                            continue84 85                        if "$ref" in content["schema"]:86                            # get the reference87                            root = openapi88                            reference = content["schema"]["$ref"].split("/")[1:]89                            for ref in reference:90                                root = root[ref]91                            # overwrite the content92                            interface["operation"]["requestBody"]["content"][content_type]["schema"] = root93 94                    # parse body parameters95                    if "schema" in interface["operation"]["requestBody"]["content"][content_type]:96                        body_schema = interface["operation"]["requestBody"]["content"][content_type]["schema"]97                        required = body_schema.get("required", [])98                        properties = body_schema.get("properties", {})99                        for name, property in properties.items():100                            tool = ToolParameter(101                                name=name,102                                label=I18nObject(en_US=name, zh_Hans=name),103                                human_description=I18nObject(104                                    en_US=property.get("description", ""), zh_Hans=property.get("description", "")105                                ),106                                type=ToolParameter.ToolParameterType.STRING,107                                required=name in required,108                                form=ToolParameter.ToolParameterForm.LLM,109                                llm_description=property.get("description", ""),110                                default=property.get("default", None),111                            )112 113                            # check if there is a type114                            typ = ApiBasedToolSchemaParser._get_tool_parameter_type(property)115                            if typ:116                                tool.type = typ117 118                            parameters.append(tool)119 120            # check if parameters is duplicated121            parameters_count = {}122            for parameter in parameters:123                if parameter.name not in parameters_count:124                    parameters_count[parameter.name] = 0125                parameters_count[parameter.name] += 1126            for name, count in parameters_count.items():127                if count > 1:128                    warning["duplicated_parameter"] = f"Parameter {name} is duplicated."129 130            # check if there is a operation id, use $path_$method as operation id if not131            if "operationId" not in interface["operation"]:132                # remove special characters like / to ensure the operation id is valid ^[a-zA-Z0-9_-]{1,64}$133                path = interface["path"]134                if interface["path"].startswith("/"):135                    path = interface["path"][1:]136                # remove special characters like / to ensure the operation id is valid ^[a-zA-Z0-9_-]{1,64}$137                path = re.sub(r"[^a-zA-Z0-9_-]", "", path)138                if not path:139                    path = str(uuid.uuid4())140 141                interface["operation"]["operationId"] = f'{path}_{interface["method"]}'142 143            bundles.append(144                ApiToolBundle(145                    server_url=server_url + interface["path"],146                    method=interface["method"],147                    summary=interface["operation"]["description"]148                    if "description" in interface["operation"]149                    else interface["operation"].get("summary", None),150                    operation_id=interface["operation"]["operationId"],151                    parameters=parameters,152                    author="",153                    icon=None,154                    openapi=interface["operation"],155                )156            )157 158        return bundles159 160    @staticmethod161    def _get_tool_parameter_type(parameter: dict) -> ToolParameter.ToolParameterType:162        parameter = parameter or {}163        typ = None164        if "type" in parameter:165            typ = parameter["type"]166        elif "schema" in parameter and "type" in parameter["schema"]:167            typ = parameter["schema"]["type"]168 169        if typ in {"integer", "number"}:170            return ToolParameter.ToolParameterType.NUMBER171        elif typ == "boolean":172            return ToolParameter.ToolParameterType.BOOLEAN173        elif typ == "string":174            return ToolParameter.ToolParameterType.STRING175 176    @staticmethod177    def parse_openapi_yaml_to_tool_bundle(178        yaml: str, extra_info: Optional[dict], warning: Optional[dict]179    ) -> list[ApiToolBundle]:180        """181        parse openapi yaml to tool bundle182 183        :param yaml: the yaml string184        :return: the tool bundle185        """186        warning = warning if warning is not None else {}187        extra_info = extra_info if extra_info is not None else {}188 189        openapi: dict = safe_load(yaml)190        if openapi is None:191            raise ToolApiSchemaError("Invalid openapi yaml.")192        return ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(openapi, extra_info=extra_info, warning=warning)193 194    @staticmethod195    def parse_swagger_to_openapi(swagger: dict, extra_info: Optional[dict], warning: Optional[dict]) -> dict:196        """197        parse swagger to openapi198 199        :param swagger: the swagger dict200        :return: the openapi dict201        """202        # convert swagger to openapi203        info = swagger.get("info", {"title": "Swagger", "description": "Swagger", "version": "1.0.0"})204 205        servers = swagger.get("servers", [])206 207        if len(servers) == 0:208            raise ToolApiSchemaError("No server found in the swagger yaml.")209 210        openapi = {211            "openapi": "3.0.0",212            "info": {213                "title": info.get("title", "Swagger"),214                "description": info.get("description", "Swagger"),215                "version": info.get("version", "1.0.0"),216            },217            "servers": swagger["servers"],218            "paths": {},219            "components": {"schemas": {}},220        }221 222        # check paths223        if "paths" not in swagger or len(swagger["paths"]) == 0:224            raise ToolApiSchemaError("No paths found in the swagger yaml.")225 226        # convert paths227        for path, path_item in swagger["paths"].items():228            openapi["paths"][path] = {}229            for method, operation in path_item.items():230                if "operationId" not in operation:231                    raise ToolApiSchemaError(f"No operationId found in operation {method} {path}.")232 233                if ("summary" not in operation or len(operation["summary"]) == 0) and (234                    "description" not in operation or len(operation["description"]) == 0235                ):236                    warning["missing_summary"] = f"No summary or description found in operation {method} {path}."237 238                openapi["paths"][path][method] = {239                    "operationId": operation["operationId"],240                    "summary": operation.get("summary", ""),241                    "description": operation.get("description", ""),242                    "parameters": operation.get("parameters", []),243                    "responses": operation.get("responses", {}),244                }245 246                if "requestBody" in operation:247                    openapi["paths"][path][method]["requestBody"] = operation["requestBody"]248 249        # convert definitions250        for name, definition in swagger["definitions"].items():251            openapi["components"]["schemas"][name] = definition252 253        return openapi254 255    @staticmethod256    def parse_openai_plugin_json_to_tool_bundle(257        json: str, extra_info: Optional[dict], warning: Optional[dict]258    ) -> list[ApiToolBundle]:259        """260        parse openapi plugin yaml to tool bundle261 262        :param json: the json string263        :return: the tool bundle264        """265        warning = warning if warning is not None else {}266        extra_info = extra_info if extra_info is not None else {}267 268        try:269            openai_plugin = json_loads(json)270            api = openai_plugin["api"]271            api_url = api["url"]272            api_type = api["type"]273        except:274            raise ToolProviderNotFoundError("Invalid openai plugin json.")275 276        if api_type != "openapi":277            raise ToolNotSupportedError("Only openapi is supported now.")278 279        # get openapi yaml280        response = get(api_url, headers={"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "}, timeout=5)281 282        if response.status_code != 200:283            raise ToolProviderNotFoundError("cannot get openapi yaml from url.")284 285        return ApiBasedToolSchemaParser.parse_openapi_yaml_to_tool_bundle(286            response.text, extra_info=extra_info, warning=warning287        )288 289    @staticmethod290    def auto_parse_to_tool_bundle(291        content: str, extra_info: Optional[dict] = None, warning: Optional[dict] = None292    ) -> tuple[list[ApiToolBundle], str]:293        """294        auto parse to tool bundle295 296        :param content: the content297        :return: tools bundle, schema_type298        """299        warning = warning if warning is not None else {}300        extra_info = extra_info if extra_info is not None else {}301 302        content = content.strip()303        loaded_content = None304        json_error = None305        yaml_error = None306 307        try:308            loaded_content = json_loads(content)309        except JSONDecodeError as e:310            json_error = e311 312        if loaded_content is None:313            try:314                loaded_content = safe_load(content)315            except YAMLError as e:316                yaml_error = e317        if loaded_content is None:318            raise ToolApiSchemaError(319                f"Invalid api schema, schema is neither json nor yaml. json error: {str(json_error)},"320                f" yaml error: {str(yaml_error)}"321            )322 323        swagger_error = None324        openapi_error = None325        openapi_plugin_error = None326        schema_type = None327 328        try:329            openapi = ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(330                loaded_content, extra_info=extra_info, warning=warning331            )332            schema_type = ApiProviderSchemaType.OPENAPI.value333            return openapi, schema_type334        except ToolApiSchemaError as e:335            openapi_error = e336 337        # openai parse error, fallback to swagger338        try:339            converted_swagger = ApiBasedToolSchemaParser.parse_swagger_to_openapi(340                loaded_content, extra_info=extra_info, warning=warning341            )342            schema_type = ApiProviderSchemaType.SWAGGER.value343            return ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(344                converted_swagger, extra_info=extra_info, warning=warning345            ), schema_type346        except ToolApiSchemaError as e:347            swagger_error = e348 349        # swagger parse error, fallback to openai plugin350        try:351            openapi_plugin = ApiBasedToolSchemaParser.parse_openai_plugin_json_to_tool_bundle(352                json_dumps(loaded_content), extra_info=extra_info, warning=warning353            )354            return openapi_plugin, ApiProviderSchemaType.OPENAI_PLUGIN.value355        except ToolNotSupportedError as e:356            # maybe it's not plugin at all357            openapi_plugin_error = e358 359        raise ToolApiSchemaError(360            f"Invalid api schema, openapi error: {str(openapi_error)}, swagger error: {str(swagger_error)},"361            f" openapi plugin error: {str(openapi_plugin_error)}"362        )363