Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
function_calling.py830 linesDownload Raw Back to utils
1"""Methods for creating function specs in the style of OpenAI Functions."""2 3from __future__ import annotations4 5import collections6import inspect7import logging8import types9import typing10import uuid11from typing import (12    TYPE_CHECKING,13    Annotated,14    Any,15    Literal,16    Union,17    cast,18    get_args,19    get_origin,20    get_type_hints,21)22 23import typing_extensions24from pydantic import BaseModel25from pydantic.errors import PydanticInvalidForJsonSchema26from pydantic.v1 import BaseModel as BaseModelV127from pydantic.v1 import Field as Field_v128from pydantic.v1 import create_model as create_model_v129from typing_extensions import TypedDict, is_typeddict30 31import langchain_core32from langchain_core._api import beta33from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, ToolMessage34from langchain_core.utils.json_schema import dereference_refs35from langchain_core.utils.pydantic import is_basemodel_subclass36 37if TYPE_CHECKING:38    from collections.abc import Callable, Mapping39 40    from langchain_core.tools import BaseTool41 42logger = logging.getLogger(__name__)43 44PYTHON_TO_JSON_TYPES = {45    "str": "string",46    "int": "integer",47    "float": "number",48    "bool": "boolean",49}50 51_ORIGIN_MAP: dict[type, Any] = {52    dict: dict,53    list: list,54    tuple: tuple,55    set: set,56    collections.abc.Iterable: typing.Iterable,57    collections.abc.Mapping: typing.Mapping,58    collections.abc.Sequence: typing.Sequence,59    collections.abc.MutableMapping: typing.MutableMapping,60}61# Add UnionType mapping for Python 3.10+62if hasattr(types, "UnionType"):63    _ORIGIN_MAP[types.UnionType] = Union64 65 66class FunctionDescription(TypedDict):67    """Representation of a callable function to send to an LLM."""68 69    name: str70    """The name of the function."""71 72    description: str73    """A description of the function."""74 75    parameters: dict76    """The parameters of the function."""77 78 79class ToolDescription(TypedDict):80    """Representation of a callable function to the OpenAI API."""81 82    type: Literal["function"]83    """The type of the tool."""84 85    function: FunctionDescription86    """The function description."""87 88 89def _rm_titles(kv: dict, prev_key: str = "") -> dict:90    """Recursively removes `'title'` fields from a JSON schema dictionary.91 92    Remove `'title'` fields from the input JSON schema dictionary,93    except when a `'title'` appears within a property definition under `'properties'`.94 95    Args:96        kv: The input JSON schema as a dictionary.97        prev_key: The key from the parent dictionary, used to identify context.98 99    Returns:100        A new dictionary with appropriate `'title'` fields removed.101    """102    new_kv = {}103 104    for k, v in kv.items():105        if k == "title":106            # If the value is a nested dict and part of a property under "properties",107            # preserve the title but continue recursion108            if isinstance(v, dict) and prev_key == "properties":109                new_kv[k] = _rm_titles(v, k)110            else:111                # Otherwise, remove this "title" key112                continue113        elif isinstance(v, dict):114            # Recurse into nested dictionaries115            new_kv[k] = _rm_titles(v, k)116        else:117            # Leave non-dict values untouched118            new_kv[k] = v119 120    return new_kv121 122 123def _convert_json_schema_to_openai_function(124    schema: dict,125    *,126    name: str | None = None,127    description: str | None = None,128    rm_titles: bool = True,129) -> FunctionDescription:130    """Converts a Pydantic model to a function description for the OpenAI API.131 132    Args:133        schema: The JSON schema to convert.134        name: The name of the function.135 136            If not provided, the title of the schema will be used.137        description: The description of the function.138 139            If not provided, the description of the schema will be used.140        rm_titles: Whether to remove titles from the schema.141 142    Returns:143        The function description.144    """145    schema = dereference_refs(schema)146    if "definitions" in schema:  # pydantic 1147        schema.pop("definitions", None)148    if "$defs" in schema:  # pydantic 2149        schema.pop("$defs", None)150    title = schema.pop("title", "")151    default_description = schema.pop("description", "")152    return {153        "name": name or title,154        "description": description or default_description,155        "parameters": _rm_titles(schema) if rm_titles else schema,156    }157 158 159def _convert_pydantic_to_openai_function(160    model: type,161    *,162    name: str | None = None,163    description: str | None = None,164    rm_titles: bool = True,165) -> FunctionDescription:166    """Converts a Pydantic model to a function description for the OpenAI API.167 168    Args:169        model: The Pydantic model to convert.170        name: The name of the function.171 172            If not provided, the title of the schema will be used.173        description: The description of the function.174 175            If not provided, the description of the schema will be used.176        rm_titles: Whether to remove titles from the schema.177 178    Raises:179        TypeError: If the model is not a Pydantic model.180        TypeError: If the model contains types that cannot be converted to JSON schema.181 182    Returns:183        The function description.184    """185    try:186        if hasattr(model, "model_json_schema"):187            schema = model.model_json_schema()  # Pydantic 2188        elif hasattr(model, "schema"):189            schema = model.schema()  # Pydantic 1190        else:191            msg = "Model must be a Pydantic model."192            raise TypeError(msg)193    except PydanticInvalidForJsonSchema as e:194        model_name = getattr(model, "__name__", str(model))195        msg = (196            f"Failed to generate JSON schema for '{model_name}': {e}\n\n"197            "Tool argument schemas must be JSON-serializable. If your schema includes "198            "custom Python classes, consider:\n"199            "  1. Converting them to Pydantic models with JSON-compatible fields\n"200            "  2. Using primitive types (str, int, float, bool, list, dict) instead\n"201            "  3. Passing the data as serialized JSON strings\n\n"202        )203        raise PydanticInvalidForJsonSchema(msg) from e204    return _convert_json_schema_to_openai_function(205        schema, name=name, description=description, rm_titles=rm_titles206    )207 208 209def _get_python_function_name(function: Callable) -> str:210    """Get the name of a Python function."""211    return function.__name__212 213 214def _convert_python_function_to_openai_function(215    function: Callable,216) -> FunctionDescription:217    """Convert a Python function to an OpenAI function-calling API compatible dict.218 219    Assumes the Python function has type hints and a docstring with a description. If220    the docstring has Google Python style argument descriptions, these will be included221    as well.222 223    Args:224        function: The Python function to convert.225 226    Returns:227        The OpenAI function description.228    """229    func_name = _get_python_function_name(function)230    model = langchain_core.tools.base.create_schema_from_function(231        func_name,232        function,233        filter_args=(),234        parse_docstring=True,235        error_on_invalid_docstring=False,236        include_injected=False,237    )238    return _convert_pydantic_to_openai_function(239        model,240        name=func_name,241        description=model.__doc__,242    )243 244 245def _convert_typed_dict_to_openai_function(typed_dict: type) -> FunctionDescription:246    visited: dict = {}247 248    model = cast(249        "type[BaseModel]",250        _convert_any_typed_dicts_to_pydantic(typed_dict, visited=visited),251    )252    return _convert_pydantic_to_openai_function(model)253 254 255_MAX_TYPED_DICT_RECURSION = 25256 257 258def _convert_any_typed_dicts_to_pydantic(259    type_: type,260    *,261    visited: dict[type, type],262    depth: int = 0,263) -> type:264    if type_ in visited:265        return visited[type_]266    if depth >= _MAX_TYPED_DICT_RECURSION:267        return type_268    if is_typeddict(type_):269        typed_dict = type_270        docstring = inspect.getdoc(typed_dict)271        # Use get_type_hints to properly resolve forward references and272        # string annotations in Python 3.14+ (PEP 649 deferred annotations).273        # include_extras=True preserves Annotated metadata.274        try:275            annotations_ = get_type_hints(typed_dict, include_extras=True)276        except Exception:277            # Fallback for edge cases where get_type_hints might fail278            annotations_ = typed_dict.__annotations__279        description, arg_descriptions = _parse_google_docstring(280            docstring, list(annotations_)281        )282        fields: dict = {}283        for arg, arg_type in annotations_.items():284            if get_origin(arg_type) in {Annotated, typing_extensions.Annotated}:285                annotated_args = get_args(arg_type)286                new_arg_type = _convert_any_typed_dicts_to_pydantic(287                    annotated_args[0], depth=depth + 1, visited=visited288                )289                field_kwargs = dict(290                    zip(("default", "description"), annotated_args[1:], strict=False)291                )292                if (field_desc := field_kwargs.get("description")) and not isinstance(293                    field_desc, str294                ):295                    msg = (296                        f"Invalid annotation for field {arg}. Third argument to "297                        f"Annotated must be a string description, received value of "298                        f"type {type(field_desc)}."299                    )300                    raise ValueError(msg)301                if arg_desc := arg_descriptions.get(arg):302                    field_kwargs["description"] = arg_desc303                fields[arg] = (new_arg_type, Field_v1(**field_kwargs))304            else:305                new_arg_type = _convert_any_typed_dicts_to_pydantic(306                    arg_type, depth=depth + 1, visited=visited307                )308                field_kwargs = {"default": ...}309                if arg_desc := arg_descriptions.get(arg):310                    field_kwargs["description"] = arg_desc311                fields[arg] = (new_arg_type, Field_v1(**field_kwargs))312        model = cast(313            "type[BaseModelV1]", create_model_v1(typed_dict.__name__, **fields)314        )315        model.__doc__ = description316        visited[typed_dict] = model317        return model318    if (origin := get_origin(type_)) and (type_args := get_args(type_)):319        subscriptable_origin = _py_38_safe_origin(origin)320        type_args = tuple(321            _convert_any_typed_dicts_to_pydantic(arg, depth=depth + 1, visited=visited)322            for arg in type_args323        )324        return cast("type", subscriptable_origin[type_args])  # type: ignore[index]325    return type_326 327 328def _format_tool_to_openai_function(tool: BaseTool) -> FunctionDescription:329    """Format tool into the OpenAI function API.330 331    Args:332        tool: The tool to format.333 334    Raises:335        ValueError: If the tool call schema is not supported.336 337    Returns:338        The function description.339    """340    is_simple_oai_tool = (341        isinstance(tool, langchain_core.tools.simple.Tool) and not tool.args_schema342    )343    if tool.tool_call_schema and not is_simple_oai_tool:344        if isinstance(tool.tool_call_schema, dict):345            return _convert_json_schema_to_openai_function(346                tool.tool_call_schema, name=tool.name, description=tool.description347            )348        if issubclass(tool.tool_call_schema, (BaseModel, BaseModelV1)):349            return _convert_pydantic_to_openai_function(350                tool.tool_call_schema, name=tool.name, description=tool.description351            )352        error_msg = (353            f"Unsupported tool call schema: {tool.tool_call_schema}. "354            "Tool call schema must be a JSON schema dict or a Pydantic model."355        )356        raise ValueError(error_msg)357    return {358        "name": tool.name,359        "description": tool.description,360        "parameters": {361            # This is a hack to get around the fact that some tools362            # do not expose an args_schema, and expect an argument363            # which is a string.364            # And Open AI does not support an array type for the365            # parameters.366            "properties": {367                "__arg1": {"title": "__arg1", "type": "string"},368            },369            "required": ["__arg1"],370            "type": "object",371        },372    }373 374 375def convert_to_openai_function(376    function: Mapping[str, Any] | type | Callable | BaseTool,377    *,378    strict: bool | None = None,379) -> dict[str, Any]:380    """Convert a raw function/class to an OpenAI function.381 382    Args:383        function: A dictionary, Pydantic `BaseModel` class, `TypedDict` class, a384            LangChain `Tool` object, or a Python function.385 386            If a dictionary is passed in, it is assumed to already be a valid OpenAI387            function, a JSON schema with top-level `title` key specified, an Anthropic388            format tool, or an Amazon Bedrock Converse format tool.389        strict: If `True`, model output is guaranteed to exactly match the JSON Schema390            provided in the function definition.391 392            If `None`, `strict` argument will not be included in function definition.393 394    Returns:395        A dict version of the passed in function which is compatible with the OpenAI396            function-calling API.397 398    Raises:399        ValueError: If function is not in a supported format.400 401    !!! warning "Behavior changed in `langchain-core` 0.3.16"402 403        `description` and `parameters` keys are now optional. Only `name` is404        required and guaranteed to be part of the output.405    """406    # an Anthropic format tool407    if isinstance(function, dict) and all(408        k in function for k in ("name", "input_schema")409    ):410        oai_function = {411            "name": function["name"],412            "parameters": function["input_schema"],413        }414        if "description" in function:415            oai_function["description"] = function["description"]416    # an Amazon Bedrock Converse format tool417    elif isinstance(function, dict) and "toolSpec" in function:418        oai_function = {419            "name": function["toolSpec"]["name"],420            "parameters": function["toolSpec"]["inputSchema"]["json"],421        }422        if "description" in function["toolSpec"]:423            oai_function["description"] = function["toolSpec"]["description"]424    # already in OpenAI function format425    elif isinstance(function, dict) and "name" in function:426        oai_function = {427            k: v428            for k, v in function.items()429            if k in {"name", "description", "parameters", "strict"}430        }431    # a JSON schema with title and description432    elif isinstance(function, dict) and "title" in function:433        function_copy = function.copy()434        oai_function = {"name": function_copy.pop("title")}435        if "description" in function_copy:436            oai_function["description"] = function_copy.pop("description")437        if function_copy and "properties" in function_copy:438            oai_function["parameters"] = function_copy439    elif isinstance(function, type) and is_basemodel_subclass(function):440        oai_function = cast("dict", _convert_pydantic_to_openai_function(function))441    elif is_typeddict(function):442        oai_function = cast(443            "dict", _convert_typed_dict_to_openai_function(cast("type", function))444        )445    elif isinstance(function, langchain_core.tools.base.BaseTool):446        oai_function = cast("dict", _format_tool_to_openai_function(function))447    elif callable(function):448        oai_function = cast(449            "dict", _convert_python_function_to_openai_function(function)450        )451    else:452        if isinstance(function, dict) and (453            "type" in function or "properties" in function454        ):455            msg = (456                f"Unsupported function\n\n{function}\n\nTo use a JSON schema as a "457                "function, it must have a top-level 'title' key to be used as the "458                "function name."459            )460            raise ValueError(msg)461        msg = (462            f"Unsupported function\n\n{function}\n\nFunctions must be passed in"463            " as Dict, pydantic.BaseModel, or Callable. If they're a dict they must"464            " either be in OpenAI function format or valid JSON schema with top-level"465            " 'title' key."466        )467        raise ValueError(msg)468 469    if strict is not None:470        if "strict" in oai_function and oai_function["strict"] != strict:471            msg = (472                f"Tool/function already has a 'strict' key with value "473                f"{oai_function['strict']} which is different from the explicit "474                f"`strict` arg received {strict=}."475            )476            raise ValueError(msg)477        oai_function["strict"] = strict478        if strict:479            # All fields must be `required`480            parameters = oai_function.get("parameters")481            if isinstance(parameters, dict):482                fields = parameters.get("properties")483                if isinstance(fields, dict) and fields:484                    parameters = dict(parameters)485                    parameters["required"] = list(fields.keys())486                    oai_function["parameters"] = parameters487 488            # As of 08/06/24, OpenAI requires that additionalProperties be supplied and489            # set to False if strict is True.490            # All properties layer needs 'additionalProperties=False'491            oai_function["parameters"] = _recursive_set_additional_properties_false(492                oai_function["parameters"]493            )494    return oai_function495 496 497# List of well known tools supported by OpenAI's chat models or responses API.498# These tools are not expected to be supported by other chat model providers499# that conform to the OpenAI function-calling API.500_WellKnownOpenAITools = (501    "function",502    "file_search",503    "computer",504    "computer_use_preview",505    "code_interpreter",506    "mcp",507    "image_generation",508    "web_search_preview",509    "web_search",510    "tool_search",511    "namespace",512)513 514 515def convert_to_openai_tool(516    tool: Mapping[str, Any] | type[BaseModel] | Callable | BaseTool,517    *,518    strict: bool | None = None,519) -> dict[str, Any]:520    """Convert a tool-like object to an OpenAI tool schema.521 522    [OpenAI tool schema reference](https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools)523 524    Args:525        tool: Either a dictionary, a `pydantic.BaseModel` class, Python function, or526            `BaseTool`.527 528            If a dictionary is passed in, it is assumed to already be a valid OpenAI529            function, a JSON schema with top-level `title` key specified, an Anthropic530            format tool, or an Amazon Bedrock Converse format tool.531        strict: If `True`, model output is guaranteed to exactly match the JSON Schema532            provided in the function definition.533 534            If `None`, `strict` argument will not be included in tool definition.535 536    Returns:537        A dict version of the passed in tool which is compatible with the OpenAI538            tool-calling API.539 540    !!! warning "Behavior changed in `langchain-core` 0.3.16"541 542        `description` and `parameters` keys are now optional. Only `name` is543        required and guaranteed to be part of the output.544 545    !!! warning "Behavior changed in `langchain-core` 0.3.44"546 547        Return OpenAI Responses API-style tools unchanged. This includes548        any dict with `"type"` in `"file_search"`, `"function"`,549        `"computer_use_preview"`, `"web_search_preview"`.550 551    !!! warning "Behavior changed in `langchain-core` 0.3.63"552 553        Added support for OpenAI's image generation built-in tool.554    """555    # Import locally to prevent circular import556    from langchain_core.tools import Tool  # noqa: PLC0415557 558    if isinstance(tool, dict):559        if tool.get("type") in _WellKnownOpenAITools:560            return tool561        # As of 03.12.25 can be "web_search_preview" or "web_search_preview_2025_03_11"562        if (tool.get("type") or "").startswith("web_search_preview"):563            return tool564    if isinstance(tool, Tool) and (tool.metadata or {}).get("type") == "custom_tool":565        oai_tool = {566            "type": "custom",567            "name": tool.name,568            "description": tool.description,569        }570        if tool.metadata is not None and "format" in tool.metadata:571            oai_tool["format"] = tool.metadata["format"]572        return oai_tool573    oai_function = convert_to_openai_function(tool, strict=strict)574    return {"type": "function", "function": oai_function}575 576 577def convert_to_json_schema(578    schema: dict[str, Any] | type[BaseModel] | Callable | BaseTool,579    *,580    strict: bool | None = None,581) -> dict[str, Any]:582    """Convert a schema representation to a JSON schema.583 584    Args:585        schema: The schema to convert.586        strict: If `True`, model output is guaranteed to exactly match the JSON Schema587            provided in the function definition.588 589            If `None`, `strict` argument will not be included in function definition.590 591    Raises:592        ValueError: If the input is not a valid OpenAI-format tool.593 594    Returns:595        A JSON schema representation of the input schema.596    """597    openai_tool = convert_to_openai_tool(schema, strict=strict)598    if (599        not isinstance(openai_tool, dict)600        or "function" not in openai_tool601        or "name" not in openai_tool["function"]602    ):603        error_message = "Input must be a valid OpenAI-format tool."604        raise ValueError(error_message)605 606    openai_function = openai_tool["function"]607    json_schema = {}608    json_schema["title"] = openai_function["name"]609 610    if "description" in openai_function:611        json_schema["description"] = openai_function["description"]612 613    if "parameters" in openai_function:614        parameters = openai_function["parameters"].copy()615        json_schema.update(parameters)616 617    return json_schema618 619 620@beta()621def tool_example_to_messages(622    input: str,623    tool_calls: list[BaseModel],624    tool_outputs: list[str] | None = None,625    *,626    ai_response: str | None = None,627) -> list[BaseMessage]:628    """Convert an example into a list of messages that can be fed into an LLM.629 630    This code is an adapter that converts a single example to a list of messages631    that can be fed into a chat model.632 633    The list of messages per example by default corresponds to:634 635    1. `HumanMessage`: contains the content from which content should be extracted.636    2. `AIMessage`: contains the extracted information from the model637    3. `ToolMessage`: contains confirmation to the model that the model requested a638        tool correctly.639 640    If `ai_response` is specified, there will be a final `AIMessage` with that641    response.642 643    The `ToolMessage` is required because some chat models are hyper-optimized for644    agents rather than for an extraction use case.645 646    Args:647        input: The user input648        tool_calls: Tool calls represented as Pydantic BaseModels649        tool_outputs: Tool call outputs.650 651            Does not need to be provided.652 653            If not provided, a placeholder value will be inserted.654        ai_response: If provided, content for a final `AIMessage`.655 656    Returns:657        A list of messages658 659    Examples:660        ```python661        from typing import Optional662        from pydantic import BaseModel, Field663        from langchain_openai import ChatOpenAI664 665 666        class Person(BaseModel):667            '''Information about a person.'''668 669            name: str | None = Field(..., description="The name of the person")670            hair_color: str | None = Field(671                ..., description="The color of the person's hair if known"672            )673            height_in_meters: str | None = Field(..., description="Height in METERS")674 675 676        examples = [677            (678                "The ocean is vast and blue. It's more than 20,000 feet deep.",679                Person(name=None, height_in_meters=None, hair_color=None),680            ),681            (682                "Fiona traveled far from France to Spain.",683                Person(name="Fiona", height_in_meters=None, hair_color=None),684            ),685        ]686 687 688        messages = []689 690        for txt, tool_call in examples:691            messages.extend(tool_example_to_messages(txt, [tool_call]))692        ```693    """694    messages: list[BaseMessage] = [HumanMessage(content=input)]695 696    openai_tool_calls = [697        {698            "id": str(uuid.uuid4()),699            "type": "function",700            "function": {701                # The name of the function right now corresponds to the name702                # of the Pydantic model. This is implicit in the API right now,703                # and will be improved over time.704                "name": tool_call.__class__.__name__,705                "arguments": tool_call.model_dump_json(),706            },707        }708        for tool_call in tool_calls709    ]710 711    messages.append(712        AIMessage(content="", additional_kwargs={"tool_calls": openai_tool_calls})713    )714    tool_outputs = tool_outputs or ["You have correctly called this tool."] * len(715        openai_tool_calls716    )717    for output, tool_call_dict in zip(tool_outputs, openai_tool_calls, strict=False):718        messages.append(ToolMessage(content=output, tool_call_id=tool_call_dict["id"]))719 720    if ai_response:721        messages.append(AIMessage(content=ai_response))722    return messages723 724 725_MIN_DOCSTRING_BLOCKS = 2726 727 728def _parse_google_docstring(729    docstring: str | None,730    args: list[str],731    *,732    error_on_invalid_docstring: bool = False,733) -> tuple[str, dict]:734    """Parse the function and argument descriptions from the docstring of a function.735 736    Assumes the function docstring follows Google Python style guide.737 738    Args:739        docstring: The docstring to parse.740        args: The list of argument names to extract descriptions for.741        error_on_invalid_docstring: Whether to raise an error if the docstring is742            invalid.743 744    Returns:745        A tuple of the function description and a dictionary of argument descriptions.746    """747    if docstring:748        docstring_blocks = docstring.split("\n\n")749        if error_on_invalid_docstring:750            filtered_annotations = {751                arg752                for arg in args753                if arg not in {"run_manager", "callbacks", "runtime", "return"}754            }755            if filtered_annotations and (756                len(docstring_blocks) < _MIN_DOCSTRING_BLOCKS757                or not any(block.startswith("Args:") for block in docstring_blocks[1:])758            ):759                msg = "Found invalid Google-Style docstring."760                raise ValueError(msg)761        descriptors = []762        args_block = None763        past_descriptors = False764        for block in docstring_blocks:765            if block.startswith("Args:"):766                args_block = block767                break768            if block.startswith(("Returns:", "Example:")):769                # Don't break in case Args come after770                past_descriptors = True771            elif not past_descriptors:772                descriptors.append(block)773            else:774                continue775        description = " ".join(descriptors).strip()776    else:777        if error_on_invalid_docstring:778            msg = "Found invalid Google-Style docstring."779            raise ValueError(msg)780        description = ""781        args_block = None782    arg_descriptions = {}783    if args_block:784        arg = None785        for line in args_block.split("\n")[1:]:786            if ":" in line:787                arg, desc = line.split(":", maxsplit=1)788                arg = arg.strip()789                arg_name, _, annotations_ = arg.partition(" ")790                if annotations_.startswith("(") and annotations_.endswith(")"):791                    arg = arg_name792                arg_descriptions[arg] = desc.strip()793            elif arg:794                arg_descriptions[arg] += " " + line.strip()795    return description, arg_descriptions796 797 798def _py_38_safe_origin(origin: type) -> type:799    return cast("type", _ORIGIN_MAP.get(origin, origin))800 801 802def _recursive_set_additional_properties_false(803    schema: dict[str, Any],804) -> dict[str, Any]:805    if isinstance(schema, dict):806        # Check if 'required' is a key at the current level or if the schema is empty,807        # in which case additionalProperties still needs to be specified.808        if (809            "required" in schema810            or ("properties" in schema and not schema["properties"])811            # Since Pydantic 2.11, it will always add `additionalProperties: True`812            # for arbitrary dictionary schemas813            # See: https://pydantic.dev/articles/pydantic-v2-11-release#changes814            # If it is already set to True, we need override it to False815            or "additionalProperties" in schema816        ):817            schema["additionalProperties"] = False818 819        # Recursively check 'properties' and 'items' if they exist820        if "anyOf" in schema:821            for sub_schema in schema["anyOf"]:822                _recursive_set_additional_properties_false(sub_schema)823        if "properties" in schema:824            for sub_schema in schema["properties"].values():825                _recursive_set_additional_properties_false(sub_schema)826        if "items" in schema:827            _recursive_set_additional_properties_false(schema["items"])828 829    return schema830 
codekingpro/portable-devtools · Team Ai