diff --git a/libs/core/langchain_core/utils/function_calling.py b/libs/core/langchain_core/utils/function_calling.py index 801aacc727a..b36db207ed6 100644 --- a/libs/core/langchain_core/utils/function_calling.py +++ b/libs/core/langchain_core/utils/function_calling.py @@ -833,8 +833,14 @@ def _recursive_set_additional_properties_false( if isinstance(schema, dict): # Check if 'required' is a key at the current level or if the schema is empty, # in which case additionalProperties still needs to be specified. - if "required" in schema or ( - "properties" in schema and not schema["properties"] + if ( + "required" in schema + or ("properties" in schema and not schema["properties"]) + # Since Pydantic 2.11, it will always add `additionalProperties: True` + # for arbitrary dictionary schemas + # See: https://pydantic.dev/articles/pydantic-v2-11-release#changes + # If it is already set to True, we need override it to False + or "additionalProperties" in schema ): schema["additionalProperties"] = False diff --git a/libs/core/tests/unit_tests/utils/test_function_calling.py b/libs/core/tests/unit_tests/utils/test_function_calling.py index f75ae304937..348cce31686 100644 --- a/libs/core/tests/unit_tests/utils/test_function_calling.py +++ b/libs/core/tests/unit_tests/utils/test_function_calling.py @@ -22,6 +22,10 @@ try: except ImportError: TypingAnnotated = ExtensionsAnnotated + +from importlib.metadata import version + +from packaging.version import parse from pydantic import BaseModel, Field from langchain_core.messages import AIMessage, HumanMessage, ToolMessage @@ -1122,3 +1126,41 @@ def test_convert_to_json_schema( ): actual = convert_to_json_schema(fn) assert actual == expected + + +def test_convert_to_openai_function_nested_strict_2() -> None: + def my_function(arg1: dict, arg2: Union[dict, None]) -> None: + """Dummy function.""" + + expected: dict = { + "name": "my_function", + "description": "Dummy function.", + "parameters": { + "type": "object", + "properties": { + "arg1": { + "additionalProperties": False, + "type": "object", + }, + "arg2": { + "anyOf": [ + {"additionalProperties": False, "type": "object"}, + {"type": "null"}, + ], + }, + }, + "required": ["arg1", "arg2"], + "additionalProperties": False, + }, + "strict": True, + } + + # there will be no extra `"additionalProperties": False` when Pydantic < 2.11 + if parse(version("pydantic")) < parse("2.11"): + del expected["parameters"]["properties"]["arg1"]["additionalProperties"] + del expected["parameters"]["properties"]["arg2"]["anyOf"][0][ + "additionalProperties" + ] + + actual = convert_to_openai_function(my_function, strict=True) + assert actual == expected