mirror of
https://github.com/hwchase17/langchain.git
synced 2025-09-24 12:01:54 +00:00
core: Cleanup Pydantic models and handle deprecation warnings (#30799)
* Simplified Pydantic handling since Pydantic v1 is not supported anymore. * Replace use of deprecated v1 methods by corresponding v2 methods. * Remove use of other deprecated methods. * Activate mypy errors on deprecated methods use. --------- Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
This commit is contained in:
committed by
GitHub
parent
29e17fbd6b
commit
7e046ea848
@@ -16,10 +16,6 @@ from langchain_core.output_parsers.openai_tools import (
|
||||
PydanticToolsParser,
|
||||
)
|
||||
from langchain_core.outputs import ChatGeneration
|
||||
from langchain_core.utils.pydantic import (
|
||||
IS_PYDANTIC_V1,
|
||||
IS_PYDANTIC_V2,
|
||||
)
|
||||
|
||||
STREAMED_MESSAGES: list = [
|
||||
AIMessageChunk(content=""),
|
||||
@@ -532,7 +528,6 @@ async def test_partial_pydantic_output_parser_async() -> None:
|
||||
assert actual == EXPECTED_STREAMED_PYDANTIC
|
||||
|
||||
|
||||
@pytest.mark.skipif(not IS_PYDANTIC_V2, reason="This test is for pydantic 2")
|
||||
def test_parse_with_different_pydantic_2_v1() -> None:
|
||||
"""Test with pydantic.v1.BaseModel from pydantic 2."""
|
||||
import pydantic
|
||||
@@ -567,7 +562,6 @@ def test_parse_with_different_pydantic_2_v1() -> None:
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(not IS_PYDANTIC_V2, reason="This test is for pydantic 2")
|
||||
def test_parse_with_different_pydantic_2_proper() -> None:
|
||||
"""Test with pydantic.BaseModel from pydantic 2."""
|
||||
import pydantic
|
||||
@@ -602,41 +596,6 @@ def test_parse_with_different_pydantic_2_proper() -> None:
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(not IS_PYDANTIC_V1, reason="This test is for pydantic 1")
|
||||
def test_parse_with_different_pydantic_1_proper() -> None:
|
||||
"""Test with pydantic.BaseModel from pydantic 1."""
|
||||
import pydantic
|
||||
|
||||
class Forecast(pydantic.BaseModel):
|
||||
temperature: int
|
||||
forecast: str
|
||||
|
||||
# Can't get pydantic to work here due to the odd typing of tryig to support
|
||||
# both v1 and v2 in the same codebase.
|
||||
parser = PydanticToolsParser(tools=[Forecast])
|
||||
message = AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_OwL7f5PE",
|
||||
"name": "Forecast",
|
||||
"args": {"temperature": 20, "forecast": "Sunny"},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
generation = ChatGeneration(
|
||||
message=message,
|
||||
)
|
||||
|
||||
assert parser.parse_result([generation]) == [
|
||||
Forecast(
|
||||
temperature=20,
|
||||
forecast="Sunny",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_max_tokens_error(caplog: Any) -> None:
|
||||
parser = PydanticToolsParser(tools=[NameCollector], first_tool_only=True)
|
||||
message = AIMessage(
|
||||
|
@@ -65,8 +65,6 @@ from langchain_core.utils.function_calling import (
|
||||
convert_to_openai_tool,
|
||||
)
|
||||
from langchain_core.utils.pydantic import (
|
||||
IS_PYDANTIC_V1,
|
||||
IS_PYDANTIC_V2,
|
||||
_create_subset_model,
|
||||
create_model_v2,
|
||||
)
|
||||
@@ -79,9 +77,11 @@ def _get_tool_call_json_schema(tool: BaseTool) -> dict:
|
||||
if isinstance(tool_schema, dict):
|
||||
return tool_schema
|
||||
|
||||
if hasattr(tool_schema, "model_json_schema"):
|
||||
if issubclass(tool_schema, BaseModel):
|
||||
return tool_schema.model_json_schema()
|
||||
return tool_schema.schema()
|
||||
if issubclass(tool_schema, BaseModelV1):
|
||||
return tool_schema.schema()
|
||||
return {}
|
||||
|
||||
|
||||
def test_unnamed_decorator() -> None:
|
||||
@@ -1853,11 +1853,14 @@ def test_args_schema_as_pydantic(pydantic_model: Any) -> None:
|
||||
)
|
||||
|
||||
input_schema = tool.get_input_schema()
|
||||
input_json_schema = (
|
||||
input_schema.model_json_schema()
|
||||
if hasattr(input_schema, "model_json_schema")
|
||||
else input_schema.schema()
|
||||
)
|
||||
if issubclass(input_schema, BaseModel):
|
||||
input_json_schema = input_schema.model_json_schema()
|
||||
elif issubclass(input_schema, BaseModelV1):
|
||||
input_json_schema = input_schema.schema()
|
||||
else:
|
||||
msg = "Unknown input schema type"
|
||||
raise TypeError(msg)
|
||||
|
||||
assert input_json_schema == {
|
||||
"properties": {
|
||||
"a": {"title": "A", "type": "integer"},
|
||||
@@ -1943,12 +1946,14 @@ def test_structured_tool_with_different_pydantic_versions(pydantic_model: Any) -
|
||||
|
||||
assert foo_tool.invoke({"a": 5, "b": "hello"}) == "foo"
|
||||
|
||||
args_schema = cast("BaseModel", foo_tool.args_schema)
|
||||
args_json_schema = (
|
||||
args_schema.model_json_schema()
|
||||
if hasattr(args_schema, "model_json_schema")
|
||||
else args_schema.schema()
|
||||
)
|
||||
args_schema = cast("type[BaseModel]", foo_tool.args_schema)
|
||||
if issubclass(args_schema, BaseModel):
|
||||
args_json_schema = args_schema.model_json_schema()
|
||||
elif issubclass(args_schema, BaseModelV1):
|
||||
args_json_schema = args_schema.schema()
|
||||
else:
|
||||
msg = "Unknown input schema type"
|
||||
raise TypeError(msg)
|
||||
assert args_json_schema == {
|
||||
"properties": {
|
||||
"a": {"title": "A", "type": "integer"},
|
||||
@@ -1960,11 +1965,13 @@ def test_structured_tool_with_different_pydantic_versions(pydantic_model: Any) -
|
||||
}
|
||||
|
||||
input_schema = foo_tool.get_input_schema()
|
||||
input_json_schema = (
|
||||
input_schema.model_json_schema()
|
||||
if hasattr(input_schema, "model_json_schema")
|
||||
else input_schema.schema()
|
||||
)
|
||||
if issubclass(input_schema, BaseModel):
|
||||
input_json_schema = input_schema.model_json_schema()
|
||||
elif issubclass(input_schema, BaseModelV1):
|
||||
input_json_schema = input_schema.schema()
|
||||
else:
|
||||
msg = "Unknown input schema type"
|
||||
raise TypeError(msg)
|
||||
assert input_json_schema == {
|
||||
"properties": {
|
||||
"a": {"title": "A", "type": "integer"},
|
||||
@@ -2020,7 +2027,6 @@ def test__is_message_content_type(obj: Any, *, expected: bool) -> None:
|
||||
assert _is_message_content_type(obj) is expected
|
||||
|
||||
|
||||
@pytest.mark.skipif(not IS_PYDANTIC_V2, reason="Testing pydantic v2.")
|
||||
@pytest.mark.parametrize("use_v1_namespace", [True, False])
|
||||
def test__get_all_basemodel_annotations_v2(*, use_v1_namespace: bool) -> None:
|
||||
A = TypeVar("A")
|
||||
@@ -2089,63 +2095,6 @@ def test__get_all_basemodel_annotations_v2(*, use_v1_namespace: bool) -> None:
|
||||
assert actual == expected
|
||||
|
||||
|
||||
@pytest.mark.skipif(not IS_PYDANTIC_V1, reason="Testing pydantic v1.")
|
||||
def test__get_all_basemodel_annotations_v1() -> None:
|
||||
A = TypeVar("A")
|
||||
|
||||
class ModelA(BaseModel, Generic[A], extra="allow"):
|
||||
a: A
|
||||
|
||||
class ModelB(ModelA[str]):
|
||||
b: Annotated[ModelA[dict[str, Any]], "foo"]
|
||||
|
||||
class Mixin:
|
||||
def foo(self) -> str:
|
||||
return "foo"
|
||||
|
||||
class ModelC(Mixin, ModelB):
|
||||
c: dict
|
||||
|
||||
expected = {"a": str, "b": Annotated[ModelA[dict[str, Any]], "foo"], "c": dict}
|
||||
actual = get_all_basemodel_annotations(ModelC)
|
||||
assert actual == expected
|
||||
|
||||
expected = {"a": str, "b": Annotated[ModelA[dict[str, Any]], "foo"]}
|
||||
actual = get_all_basemodel_annotations(ModelB)
|
||||
assert actual == expected
|
||||
|
||||
expected = {"a": Any}
|
||||
actual = get_all_basemodel_annotations(ModelA)
|
||||
assert actual == expected
|
||||
|
||||
expected = {"a": int}
|
||||
actual = get_all_basemodel_annotations(ModelA[int])
|
||||
assert actual == expected
|
||||
|
||||
D = TypeVar("D", bound=Union[str, int])
|
||||
|
||||
class ModelD(ModelC, Generic[D]):
|
||||
d: Optional[D]
|
||||
|
||||
expected = {
|
||||
"a": str,
|
||||
"b": Annotated[ModelA[dict[str, Any]], "foo"],
|
||||
"c": dict,
|
||||
"d": Union[str, int, None],
|
||||
}
|
||||
actual = get_all_basemodel_annotations(ModelD)
|
||||
assert actual == expected
|
||||
|
||||
expected = {
|
||||
"a": str,
|
||||
"b": Annotated[ModelA[dict[str, Any]], "foo"],
|
||||
"c": dict,
|
||||
"d": Union[int, None],
|
||||
}
|
||||
actual = get_all_basemodel_annotations(ModelD[int])
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test_get_all_basemodel_annotations_aliases() -> None:
|
||||
class CalculatorInput(BaseModel):
|
||||
a: int = Field(description="first number", alias="A")
|
||||
@@ -2226,7 +2175,6 @@ def test_create_retriever_tool() -> None:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not IS_PYDANTIC_V2, reason="Testing pydantic v2.")
|
||||
def test_tool_args_schema_pydantic_v2_with_metadata() -> None:
|
||||
from pydantic import BaseModel as BaseModelV2
|
||||
from pydantic import Field as FieldV2
|
||||
|
@@ -3,13 +3,9 @@
|
||||
import warnings
|
||||
from typing import Any, Optional
|
||||
|
||||
import pytest
|
||||
from pydantic import ConfigDict
|
||||
|
||||
from langchain_core.utils.pydantic import (
|
||||
IS_PYDANTIC_V1,
|
||||
IS_PYDANTIC_V2,
|
||||
PYDANTIC_VERSION,
|
||||
_create_subset_model_v2,
|
||||
create_model_v2,
|
||||
get_fields,
|
||||
@@ -96,50 +92,29 @@ def test_with_aliases() -> None:
|
||||
|
||||
def test_is_basemodel_subclass() -> None:
|
||||
"""Test pydantic."""
|
||||
if IS_PYDANTIC_V1:
|
||||
from pydantic import BaseModel as BaseModelV1Proper
|
||||
from pydantic import BaseModel as BaseModelV2
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
|
||||
assert is_basemodel_subclass(BaseModelV1Proper)
|
||||
elif IS_PYDANTIC_V2:
|
||||
from pydantic import BaseModel as BaseModelV2
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
|
||||
assert is_basemodel_subclass(BaseModelV2)
|
||||
|
||||
assert is_basemodel_subclass(BaseModelV1)
|
||||
else:
|
||||
msg = f"Unsupported Pydantic version: {PYDANTIC_VERSION.major}"
|
||||
raise ValueError(msg)
|
||||
assert is_basemodel_subclass(BaseModelV2)
|
||||
assert is_basemodel_subclass(BaseModelV1)
|
||||
|
||||
|
||||
def test_is_basemodel_instance() -> None:
|
||||
"""Test pydantic."""
|
||||
if IS_PYDANTIC_V1:
|
||||
from pydantic import BaseModel as BaseModelV1Proper
|
||||
from pydantic import BaseModel as BaseModelV2
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
|
||||
class FooV1(BaseModelV1Proper):
|
||||
x: int
|
||||
class Foo(BaseModelV2):
|
||||
x: int
|
||||
|
||||
assert is_basemodel_instance(FooV1(x=5))
|
||||
elif IS_PYDANTIC_V2:
|
||||
from pydantic import BaseModel as BaseModelV2
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
assert is_basemodel_instance(Foo(x=5))
|
||||
|
||||
class Foo(BaseModelV2):
|
||||
x: int
|
||||
class Bar(BaseModelV1):
|
||||
x: int
|
||||
|
||||
assert is_basemodel_instance(Foo(x=5))
|
||||
|
||||
class Bar(BaseModelV1):
|
||||
x: int
|
||||
|
||||
assert is_basemodel_instance(Bar(x=5))
|
||||
else:
|
||||
msg = f"Unsupported Pydantic version: {PYDANTIC_VERSION.major}"
|
||||
raise ValueError(msg)
|
||||
assert is_basemodel_instance(Bar(x=5))
|
||||
|
||||
|
||||
@pytest.mark.skipif(not IS_PYDANTIC_V2, reason="Only tests Pydantic v2")
|
||||
def test_with_field_metadata() -> None:
|
||||
"""Test pydantic with field metadata."""
|
||||
from pydantic import BaseModel as BaseModelV2
|
||||
@@ -168,18 +143,6 @@ def test_with_field_metadata() -> None:
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.skipif(not IS_PYDANTIC_V1, reason="Only tests Pydantic v1")
|
||||
def test_fields_pydantic_v1() -> None:
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Foo(BaseModel):
|
||||
x: int
|
||||
|
||||
fields = get_fields(Foo)
|
||||
assert fields == {"x": Foo.model_fields["x"]}
|
||||
|
||||
|
||||
@pytest.mark.skipif(not IS_PYDANTIC_V2, reason="Only tests Pydantic v2")
|
||||
def test_fields_pydantic_v2_proper() -> None:
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -190,7 +153,6 @@ def test_fields_pydantic_v2_proper() -> None:
|
||||
assert fields == {"x": Foo.model_fields["x"]}
|
||||
|
||||
|
||||
@pytest.mark.skipif(not IS_PYDANTIC_V2, reason="Only tests Pydantic v2")
|
||||
def test_fields_pydantic_v1_from_2() -> None:
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
|
@@ -16,10 +16,6 @@ from langchain_core.utils import (
|
||||
guard_import,
|
||||
)
|
||||
from langchain_core.utils._merge import merge_dicts
|
||||
from langchain_core.utils.pydantic import (
|
||||
IS_PYDANTIC_V1,
|
||||
IS_PYDANTIC_V2,
|
||||
)
|
||||
from langchain_core.utils.utils import secret_from_env
|
||||
|
||||
|
||||
@@ -214,7 +210,6 @@ def test_guard_import_failure(
|
||||
guard_import(module_name, pip_name=pip_name, package=package)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not IS_PYDANTIC_V2, reason="Requires pydantic 2")
|
||||
def test_get_pydantic_field_names_v1_in_2() -> None:
|
||||
from pydantic.v1 import BaseModel as PydanticV1BaseModel
|
||||
from pydantic.v1 import Field
|
||||
@@ -229,7 +224,6 @@ def test_get_pydantic_field_names_v1_in_2() -> None:
|
||||
assert result == expected
|
||||
|
||||
|
||||
@pytest.mark.skipif(not IS_PYDANTIC_V2, reason="Requires pydantic 2")
|
||||
def test_get_pydantic_field_names_v2_in_2() -> None:
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -243,20 +237,6 @@ def test_get_pydantic_field_names_v2_in_2() -> None:
|
||||
assert result == expected
|
||||
|
||||
|
||||
@pytest.mark.skipif(not IS_PYDANTIC_V1, reason="Requires pydantic 1")
|
||||
def test_get_pydantic_field_names_v1() -> None:
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class PydanticModel(BaseModel):
|
||||
field1: str
|
||||
field2: int
|
||||
alias_field: int = Field(alias="aliased_field")
|
||||
|
||||
result = get_pydantic_field_names(PydanticModel)
|
||||
expected = {"field1", "field2", "aliased_field", "alias_field"}
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_from_env_with_env_variable() -> None:
|
||||
key = "TEST_KEY"
|
||||
value = "test_value"
|
||||
|
Reference in New Issue
Block a user