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:
ccurme
2025-03-31 14:22:57 -04:00
committed by GitHub
parent e8be3cca5c
commit 0c623045b5
8 changed files with 212 additions and 147 deletions

View File

@@ -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",
}

View File

@@ -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

View File

@@ -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
'''
# ---

View File

@@ -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: