mirror of
https://github.com/hwchase17/langchain.git
synced 2025-09-14 05:56:40 +00:00
core[patch]: pydantic 2.11 compat (#30554)
Release notes: https://pydantic.dev/articles/pydantic-v2-11-release Covered here: - We no longer access `model_fields` on class instances (that is now deprecated); - Update schema normalization for Pydantic version testing to reflect changes to generated JSON schema (addition of `"additionalProperties": True` for dict types with value Any or object). ## Considerations: ### Changes to JSON schema generation #### Tool-calling / structured outputs This may impact tool-calling + structured outputs for some providers, but schema generation only changes if you have parameters of the form `dict`, `dict[str, Any]`, `dict[str, object]`, etc. If dict parameters are typed my understanding is there are no changes. For OpenAI for example, untyped dicts work for structured outputs with default settings before and after updating Pydantic, and error both before/after if `strict=True`. ### Use of `model_fields` There is one spot where we previously accessed `super(cls, self).model_fields`, where `cls` is an object in the MRO. This was done for the purpose of tracking aliases in secrets. I've updated this to always be `type(self).model_fields`-- see comment in-line for detail. --------- Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
This commit is contained in:
@@ -237,8 +237,8 @@ def test_mustache_prompt_from_template(snapshot: SnapshotAssertion) -> None:
|
||||
is a test."""
|
||||
)
|
||||
assert prompt.input_variables == ["foo"]
|
||||
assert prompt.get_input_jsonschema() == {
|
||||
"properties": {"foo": {"default": None, "title": "Foo", "type": "object"}},
|
||||
assert _normalize_schema(prompt.get_input_jsonschema()) == {
|
||||
"properties": {"foo": {"title": "Foo", "type": "object"}},
|
||||
"title": "PromptInput",
|
||||
"type": "object",
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
from typing import Any
|
||||
from typing import Any, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -97,6 +97,39 @@ def _schema(obj: Any) -> dict:
|
||||
return schema_
|
||||
|
||||
|
||||
def _remove_additionalproperties_from_untyped_dicts(schema: dict) -> dict[str, Any]:
|
||||
"""Remove `"additionalProperties": True` from dicts in the schema.
|
||||
|
||||
Pydantic 2.11 and later versions include `"additionalProperties": True` when
|
||||
generating JSON schemas for dict properties with `Any` or `object` values.
|
||||
"""
|
||||
|
||||
def _remove_dict_additional_props(
|
||||
obj: Union[dict[str, Any], list[Any]], inside_properties: bool = False
|
||||
) -> None:
|
||||
if isinstance(obj, dict):
|
||||
if (
|
||||
inside_properties
|
||||
and obj.get("type") == "object"
|
||||
and obj.get("additionalProperties") is True
|
||||
):
|
||||
obj.pop("additionalProperties", None)
|
||||
|
||||
# Recursively scan children
|
||||
for key, value in obj.items():
|
||||
# We are "inside_properties" if the *current* key is "properties",
|
||||
# or if we were already inside properties in the caller.
|
||||
next_inside_properties = inside_properties or (key == "properties")
|
||||
_remove_dict_additional_props(value, next_inside_properties)
|
||||
|
||||
elif isinstance(obj, list):
|
||||
for item in obj:
|
||||
_remove_dict_additional_props(item, inside_properties)
|
||||
|
||||
_remove_dict_additional_props(schema, inside_properties=False)
|
||||
return schema
|
||||
|
||||
|
||||
def _normalize_schema(obj: Any) -> dict[str, Any]:
|
||||
"""Generate a schema and normalize it.
|
||||
|
||||
@@ -117,4 +150,5 @@ def _normalize_schema(obj: Any) -> dict[str, Any]:
|
||||
remove_all_none_default(data)
|
||||
replace_all_of_with_ref(data)
|
||||
_remove_enum(data)
|
||||
_remove_additionalproperties_from_untyped_dicts(data)
|
||||
return data
|
||||
|
@@ -30,41 +30,6 @@
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_triple_nested_subgraph_mermaid[mermaid]
|
||||
'''
|
||||
---
|
||||
config:
|
||||
flowchart:
|
||||
curve: linear
|
||||
---
|
||||
graph TD;
|
||||
__start__([<p>__start__</p>]):::first
|
||||
parent_1(parent_1)
|
||||
parent_2(parent_2)
|
||||
__end__([<p>__end__</p>]):::last
|
||||
__start__ --> parent_1;
|
||||
child_child_2 --> parent_2;
|
||||
parent_1 --> child_child_1_grandchild_1;
|
||||
parent_2 --> __end__;
|
||||
subgraph child
|
||||
child_child_2(child_2)
|
||||
child_child_1_grandchild_2 --> child_child_2;
|
||||
subgraph child_1
|
||||
child_child_1_grandchild_1(grandchild_1)
|
||||
child_child_1_grandchild_2(grandchild_2<hr/><small><em>__interrupt = before</em></small>)
|
||||
child_child_1_grandchild_1_greatgrandchild --> child_child_1_grandchild_2;
|
||||
subgraph grandchild_1
|
||||
child_child_1_grandchild_1_greatgrandchild(greatgrandchild)
|
||||
child_child_1_grandchild_1 --> child_child_1_grandchild_1_greatgrandchild;
|
||||
end
|
||||
end
|
||||
end
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
classDef last fill:#bfb6fc
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_graph_mermaid_duplicate_nodes[mermaid]
|
||||
'''
|
||||
graph TD;
|
||||
@@ -2148,3 +2113,38 @@
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_triple_nested_subgraph_mermaid[mermaid]
|
||||
'''
|
||||
---
|
||||
config:
|
||||
flowchart:
|
||||
curve: linear
|
||||
---
|
||||
graph TD;
|
||||
__start__([<p>__start__</p>]):::first
|
||||
parent_1(parent_1)
|
||||
parent_2(parent_2)
|
||||
__end__([<p>__end__</p>]):::last
|
||||
__start__ --> parent_1;
|
||||
child_child_2 --> parent_2;
|
||||
parent_1 --> child_child_1_grandchild_1;
|
||||
parent_2 --> __end__;
|
||||
subgraph child
|
||||
child_child_2(child_2)
|
||||
child_child_1_grandchild_2 --> child_child_2;
|
||||
subgraph child_1
|
||||
child_child_1_grandchild_1(grandchild_1)
|
||||
child_child_1_grandchild_2(grandchild_2<hr/><small><em>__interrupt = before</em></small>)
|
||||
child_child_1_grandchild_1_greatgrandchild --> child_child_1_grandchild_2;
|
||||
subgraph grandchild_1
|
||||
child_child_1_grandchild_1_greatgrandchild(greatgrandchild)
|
||||
child_child_1_grandchild_1 --> child_child_1_grandchild_1_greatgrandchild;
|
||||
end
|
||||
end
|
||||
end
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
classDef last fill:#bfb6fc
|
||||
|
||||
'''
|
||||
# ---
|
||||
|
@@ -1,6 +1,7 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Callable, Optional, Union
|
||||
|
||||
import pydantic
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -19,6 +20,8 @@ from langchain_core.runnables.utils import ConfigurableFieldSpec, Input, Output
|
||||
from langchain_core.tracers import Run
|
||||
from tests.unit_tests.pydantic_utils import _schema
|
||||
|
||||
PYDANTIC_VERSION = tuple(map(int, pydantic.__version__.split(".")))
|
||||
|
||||
|
||||
def test_interfaces() -> None:
|
||||
history = InMemoryChatMessageHistory()
|
||||
@@ -484,10 +487,13 @@ def test_get_output_schema() -> None:
|
||||
)
|
||||
output_type = with_history.get_output_schema()
|
||||
|
||||
assert _schema(output_type) == {
|
||||
expected_schema: dict = {
|
||||
"title": "RunnableWithChatHistoryOutput",
|
||||
"type": "object",
|
||||
}
|
||||
if PYDANTIC_VERSION >= (2, 11):
|
||||
expected_schema["additionalProperties"] = True
|
||||
assert _schema(output_type) == expected_schema
|
||||
|
||||
|
||||
def test_get_input_schema_input_messages() -> None:
|
||||
|
Reference in New Issue
Block a user