libs/langchain/langchain_classic/chains/openai_functions/openapi.py PYTHON 368 lines View on github.com → Search inside
1from __future__ import annotations23import json4import logging5import re6from collections import defaultdict7from collections.abc import Callable8from typing import TYPE_CHECKING, Any910import requests11from langchain_core._api import deprecated12from langchain_core.callbacks import CallbackManagerForChainRun13from langchain_core.language_models import BaseLanguageModel14from langchain_core.output_parsers.openai_functions import JsonOutputFunctionsParser15from langchain_core.prompts import BasePromptTemplate, ChatPromptTemplate16from langchain_core.utils.input import get_colored_text17from requests import JSONDecodeError, Response18from typing_extensions import override1920from langchain_classic.chains.base import Chain21from langchain_classic.chains.llm import LLMChain22from langchain_classic.chains.sequential import SequentialChain2324if TYPE_CHECKING:25    from langchain_community.utilities.openapi import OpenAPISpec26    from openapi_pydantic import Parameter2728_logger = logging.getLogger(__name__)2930_OPENAPI_REPLACEMENT = (31    "Bind your OpenAPI operations as tools on a chat model with "32    "`ChatModel.bind_tools(...)` and execute the resulting tool calls with an "33    "HTTP client (e.g. `requests` or `httpx`)."34)353637def _format_url(url: str, path_params: dict) -> str:38    expected_path_param = re.findall(r"{(.*?)}", url)39    new_params = {}40    for param in expected_path_param:41        clean_param = param.lstrip(".;").rstrip("*")42        val = path_params[clean_param]43        if isinstance(val, list):44            if param[0] == ".":45                sep = "." if param[-1] == "*" else ","46                new_val = "." + sep.join(val)47            elif param[0] == ";":48                sep = f"{clean_param}=" if param[-1] == "*" else ","49                new_val = f"{clean_param}=" + sep.join(val)50            else:51                new_val = ",".join(val)52        elif isinstance(val, dict):53            kv_sep = "=" if param[-1] == "*" else ","54            kv_strs = [kv_sep.join((k, v)) for k, v in val.items()]55            if param[0] == ".":56                sep = "."57                new_val = "."58            elif param[0] == ";":59                sep = ";"60                new_val = ";"61            else:62                sep = ","63                new_val = ""64            new_val += sep.join(kv_strs)65        elif param[0] == ".":66            new_val = f".{val}"67        elif param[0] == ";":68            new_val = f";{clean_param}={val}"69        else:70            new_val = val71        new_params[param] = new_val72    return url.format(**new_params)737475def _openapi_params_to_json_schema(params: list[Parameter], spec: OpenAPISpec) -> dict:76    properties = {}77    required = []78    for p in params:79        if p.param_schema:80            schema = spec.get_schema(p.param_schema)81        else:82            media_type_schema = next(iter(p.content.values())).media_type_schema83            schema = spec.get_schema(media_type_schema)84        if p.description and not schema.description:85            schema.description = p.description86        properties[p.name] = json.loads(schema.json(exclude_none=True))87        if p.required:88            required.append(p.name)89    return {"type": "object", "properties": properties, "required": required}909192@deprecated(93    since="1.0.4",94    removal="2.0.0",95    addendum=_OPENAPI_REPLACEMENT,96)97def openapi_spec_to_openai_fn(98    spec: OpenAPISpec,99) -> tuple[list[dict[str, Any]], Callable]:100    """OpenAPI spec to OpenAI function JSON Schema.101102    Convert a valid OpenAPI spec to the JSON Schema format expected for OpenAI103    functions.104105    Args:106        spec: OpenAPI spec to convert.107108    Returns:109        Tuple of the OpenAI functions JSON schema and a default function for executing110            a request based on the OpenAI function schema.111    """112    try:113        from langchain_community.tools import APIOperation114    except ImportError as e:115        msg = (116            "Could not import langchain_community.tools. "117            "Please install it with `pip install langchain-community`."118        )119        raise ImportError(msg) from e120121    if not spec.paths:122        return [], lambda: None123    functions = []124    _name_to_call_map = {}125    for path in spec.paths:126        path_params = {127            (p.name, p.param_in): p for p in spec.get_parameters_for_path(path)128        }129        for method in spec.get_methods_for_path(path):130            request_args = {}131            op = spec.get_operation(path, method)132            op_params = path_params.copy()133            for param in spec.get_parameters_for_operation(op):134                op_params[(param.name, param.param_in)] = param135            params_by_type = defaultdict(list)136            for name_loc, p in op_params.items():137                params_by_type[name_loc[1]].append(p)138            param_loc_to_arg_name = {139                "query": "params",140                "header": "headers",141                "cookie": "cookies",142                "path": "path_params",143            }144            for param_loc, arg_name in param_loc_to_arg_name.items():145                if params_by_type[param_loc]:146                    request_args[arg_name] = _openapi_params_to_json_schema(147                        params_by_type[param_loc],148                        spec,149                    )150            request_body = spec.get_request_body_for_operation(op)151            # TODO: Support more MIME types.152            if request_body and request_body.content:153                media_types = {}154                for media_type, media_type_object in request_body.content.items():155                    if media_type_object.media_type_schema:156                        schema = spec.get_schema(media_type_object.media_type_schema)157                        media_types[media_type] = json.loads(158                            schema.json(exclude_none=True),159                        )160                if len(media_types) == 1:161                    media_type, schema_dict = next(iter(media_types.items()))162                    key = "json" if media_type == "application/json" else "data"163                    request_args[key] = schema_dict164                elif len(media_types) > 1:165                    request_args["data"] = {"anyOf": list(media_types.values())}166167            api_op = APIOperation.from_openapi_spec(spec, path, method)168            fn = {169                "name": api_op.operation_id,170                "description": api_op.description,171                "parameters": {172                    "type": "object",173                    "properties": request_args,174                },175            }176            functions.append(fn)177            _name_to_call_map[fn["name"]] = {178                "method": method,179                "url": api_op.base_url + api_op.path,180            }181182    def default_call_api(183        name: str,184        fn_args: dict,185        headers: dict | None = None,186        params: dict | None = None,187        timeout: int | None = 30,188        **kwargs: Any,189    ) -> Any:190        method = _name_to_call_map[name]["method"]191        url = _name_to_call_map[name]["url"]192        path_params = fn_args.pop("path_params", {})193        url = _format_url(url, path_params)194        if "data" in fn_args and isinstance(fn_args["data"], dict):195            fn_args["data"] = json.dumps(fn_args["data"])196        _kwargs = {**fn_args, **kwargs}197        if headers is not None:198            if "headers" in _kwargs:199                _kwargs["headers"].update(headers)200            else:201                _kwargs["headers"] = headers202        if params is not None:203            if "params" in _kwargs:204                _kwargs["params"].update(params)205            else:206                _kwargs["params"] = params207        return requests.request(method, url, **_kwargs, timeout=timeout)208209    return functions, default_call_api210211212@deprecated(213    since="1.0.4",214    removal="2.0.0",215    addendum=_OPENAPI_REPLACEMENT,216)217class SimpleRequestChain(Chain):218    """Chain for making a simple request to an API endpoint."""219220    request_method: Callable221    """Method to use for making the request."""222223    output_key: str = "response"224    """Key to use for the output of the request."""225226    input_key: str = "function"227    """Key to use for the input of the request."""228229    @property230    @override231    def input_keys(self) -> list[str]:232        return [self.input_key]233234    @property235    @override236    def output_keys(self) -> list[str]:237        return [self.output_key]238239    def _call(240        self,241        inputs: dict[str, Any],242        run_manager: CallbackManagerForChainRun | None = None,243    ) -> dict[str, Any]:244        """Run the logic of this chain and return the output."""245        _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()246        name = inputs[self.input_key].pop("name")247        args = inputs[self.input_key].pop("arguments")248        _pretty_name = get_colored_text(name, "green")249        _pretty_args = get_colored_text(json.dumps(args, indent=2), "green")250        _text = f"Calling endpoint {_pretty_name} with arguments:\n" + _pretty_args251        _run_manager.on_text(_text)252        api_response: Response = self.request_method(name, args)253        if api_response.status_code != requests.codes.ok:254            response = (255                f"{api_response.status_code}: {api_response.reason}"256                f"\nFor {name} "257                f"Called with args: {args.get('params', '')}"258            )259        else:260            try:261                response = api_response.json()262            except JSONDecodeError:263                response = api_response.text264            except Exception:265                _logger.exception("Unexpected error parsing response as JSON")266                response = api_response.text267        return {self.output_key: response}268269270@deprecated(271    since="0.2.13",272    removal="2.0.0",273    addendum=_OPENAPI_REPLACEMENT,274)275def get_openapi_chain(276    spec: OpenAPISpec | str,277    llm: BaseLanguageModel | None = None,278    prompt: BasePromptTemplate | None = None,279    request_chain: Chain | None = None,280    llm_chain_kwargs: dict | None = None,281    verbose: bool = False,  # noqa: FBT001,FBT002282    headers: dict | None = None,283    params: dict | None = None,284    **kwargs: Any,285) -> SequentialChain:286    r"""Create a chain for querying an API from a OpenAPI spec.287288    !!! warning "Deprecated"289        This function and all related utilities in this module are deprecated.290        Use LLM tool calling features directly with an HTTP client instead.291292    Args:293        spec: OpenAPISpec or url/file/text string corresponding to one.294        llm: language model, should be an OpenAI function-calling model.295        prompt: Main prompt template to use.296        request_chain: Chain for taking the functions output and executing the request.297        params: Request parameters.298        headers: Request headers.299        verbose: Whether to run the chain in verbose mode.300        llm_chain_kwargs: LLM chain additional keyword arguments.301        **kwargs: Additional keyword arguments to pass to the chain.302303    """304    try:305        from langchain_community.utilities.openapi import OpenAPISpec306    except ImportError as e:307        msg = (308            "Could not import langchain_community.utilities.openapi. "309            "Please install it with `pip install langchain-community`."310        )311        raise ImportError(msg) from e312    if isinstance(spec, str):313        for conversion in (314            OpenAPISpec.from_url,315            OpenAPISpec.from_file,316            OpenAPISpec.from_text,317        ):318            try:319                spec = conversion(spec)320                break321            except ImportError:322                raise323            except Exception:  # noqa: BLE001324                _logger.debug(325                    "Parse spec failed for OpenAPISpec.%s",326                    conversion.__name__,327                    exc_info=True,328                )329        if isinstance(spec, str):330            msg = f"Unable to parse spec from source {spec}"331            raise ValueError(msg)  # noqa: TRY004332    openai_fns, call_api_fn = openapi_spec_to_openai_fn(spec)333    if not llm:334        msg = (335            "Must provide an LLM for this chain.For example,\n"336            "from langchain_openai import ChatOpenAI\n"337            "model = ChatOpenAI()\n"338        )339        raise ValueError(msg)340    prompt = prompt or ChatPromptTemplate.from_template(341        "Use the provided API's to respond to this user query:\n\n{query}",342    )343    llm_chain = LLMChain(344        llm=llm,345        prompt=prompt,346        llm_kwargs={"functions": openai_fns},347        output_parser=JsonOutputFunctionsParser(args_only=False),348        output_key="function",349        verbose=verbose,350        **(llm_chain_kwargs or {}),351    )352    request_chain = request_chain or SimpleRequestChain(353        request_method=lambda name, args: call_api_fn(354            name,355            args,356            headers=headers,357            params=params,358        ),359        verbose=verbose,360    )361    return SequentialChain(362        chains=[llm_chain, request_chain],363        input_variables=llm_chain.input_keys,364        output_variables=["response"],365        verbose=verbose,366        **kwargs,367    )

Code quality findings 13

Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(val, list):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(val, dict):
Ensure functions have docstrings for documentation
missing-docstring
def openapi_spec_to_openai_fn(
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
request_args["data"] = {"anyOf": list(media_types.values())}
Ensure functions have docstrings for documentation
missing-docstring
def default_call_api(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if "data" in fn_args and isinstance(fn_args["data"], dict):
Ensure functions have docstrings for documentation
missing-docstring
def input_keys(self) -> list[str]:
Ensure functions have docstrings for documentation
missing-docstring
def output_keys(self) -> list[str]:
Catch specific exceptions instead of Exception to avoid masking bugs
broad-except
except Exception:
Ensure functions have docstrings for documentation
missing-docstring
def get_openapi_chain(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(spec, str):
Catch specific exceptions instead of Exception to avoid masking bugs
broad-except
except Exception: # noqa: BLE001
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(spec, str):

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.