mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-17 21:25:46 +02:00
removing langchain-core pydantic utilities
This commit is contained in:
@@ -1,10 +1,181 @@
|
||||
import sys
|
||||
import typing
|
||||
import warnings
|
||||
from contextlib import nullcontext
|
||||
from dataclasses import is_dataclass
|
||||
from typing import Any, Optional
|
||||
from functools import lru_cache
|
||||
from typing import (
|
||||
Any,
|
||||
Optional,
|
||||
Union,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
import typing_extensions
|
||||
from pydantic import BaseModel
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
RootModel,
|
||||
)
|
||||
from pydantic import (
|
||||
create_model as _create_model_base,
|
||||
)
|
||||
from pydantic.fields import FieldInfo
|
||||
from pydantic.json_schema import (
|
||||
DEFAULT_REF_TEMPLATE,
|
||||
GenerateJsonSchema,
|
||||
JsonSchemaMode,
|
||||
)
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
@overload
|
||||
def get_fields(model: type[BaseModel]) -> dict[str, FieldInfo]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def get_fields(model: BaseModel) -> dict[str, FieldInfo]: ...
|
||||
|
||||
|
||||
def get_fields(
|
||||
model: Union[type[BaseModel], BaseModel],
|
||||
) -> dict[str, FieldInfo]:
|
||||
"""Get the field names of a Pydantic model."""
|
||||
if hasattr(model, "model_fields"):
|
||||
return model.model_fields
|
||||
|
||||
if hasattr(model, "__fields__"):
|
||||
return model.__fields__ # type: ignore[return-value]
|
||||
msg = f"Expected a Pydantic model. Got {type(model)}"
|
||||
raise TypeError(msg)
|
||||
|
||||
|
||||
_SchemaConfig = ConfigDict(
|
||||
arbitrary_types_allowed=True, frozen=True, protected_namespaces=()
|
||||
)
|
||||
|
||||
NO_DEFAULT = object()
|
||||
|
||||
|
||||
def _create_root_model(
|
||||
name: str,
|
||||
type_: Any,
|
||||
module_name: Optional[str] = None,
|
||||
default_: object = NO_DEFAULT,
|
||||
) -> type[BaseModel]:
|
||||
"""Create a base class."""
|
||||
|
||||
def schema(
|
||||
cls: type[BaseModel],
|
||||
by_alias: bool = True, # noqa: FBT001,FBT002
|
||||
ref_template: str = DEFAULT_REF_TEMPLATE,
|
||||
) -> dict[str, Any]:
|
||||
# Complains about schema not being defined in superclass
|
||||
schema_ = super(cls, cls).schema( # type: ignore[misc]
|
||||
by_alias=by_alias, ref_template=ref_template
|
||||
)
|
||||
schema_["title"] = name
|
||||
return schema_
|
||||
|
||||
def model_json_schema(
|
||||
cls: type[BaseModel],
|
||||
by_alias: bool = True, # noqa: FBT001,FBT002
|
||||
ref_template: str = DEFAULT_REF_TEMPLATE,
|
||||
schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
|
||||
mode: JsonSchemaMode = "validation",
|
||||
) -> dict[str, Any]:
|
||||
# Complains about model_json_schema not being defined in superclass
|
||||
schema_ = super(cls, cls).model_json_schema( # type: ignore[misc]
|
||||
by_alias=by_alias,
|
||||
ref_template=ref_template,
|
||||
schema_generator=schema_generator,
|
||||
mode=mode,
|
||||
)
|
||||
schema_["title"] = name
|
||||
return schema_
|
||||
|
||||
base_class_attributes = {
|
||||
"__annotations__": {"root": type_},
|
||||
"model_config": ConfigDict(arbitrary_types_allowed=True),
|
||||
"schema": classmethod(schema),
|
||||
"model_json_schema": classmethod(model_json_schema),
|
||||
"__module__": module_name or "langchain_core.runnables.utils",
|
||||
}
|
||||
|
||||
if default_ is not NO_DEFAULT:
|
||||
base_class_attributes["root"] = default_
|
||||
with warnings.catch_warnings():
|
||||
custom_root_type = type(name, (RootModel,), base_class_attributes)
|
||||
return cast("type[BaseModel]", custom_root_type)
|
||||
|
||||
|
||||
@lru_cache(maxsize=256)
|
||||
def _create_root_model_cached(
|
||||
model_name: str,
|
||||
type_: Any,
|
||||
*,
|
||||
module_name: Optional[str] = None,
|
||||
default_: object = NO_DEFAULT,
|
||||
) -> type[BaseModel]:
|
||||
return _create_root_model(
|
||||
model_name, type_, default_=default_, module_name=module_name
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=256)
|
||||
def _create_model_cached(
|
||||
model_name: str,
|
||||
/,
|
||||
**field_definitions: Any,
|
||||
) -> type[BaseModel]:
|
||||
return _create_model_base(
|
||||
model_name,
|
||||
__config__=_SchemaConfig,
|
||||
**_remap_field_definitions(field_definitions),
|
||||
)
|
||||
|
||||
|
||||
# Reserved names should capture all the `public` names / methods that are
|
||||
# used by BaseModel internally. This will keep the reserved names up-to-date.
|
||||
# For reference, the reserved names are:
|
||||
# "construct", "copy", "dict", "from_orm", "json", "parse_file", "parse_obj",
|
||||
# "parse_raw", "schema", "schema_json", "update_forward_refs", "validate",
|
||||
# "model_computed_fields", "model_config", "model_construct", "model_copy",
|
||||
# "model_dump", "model_dump_json", "model_extra", "model_fields",
|
||||
# "model_fields_set", "model_json_schema", "model_parametrized_name",
|
||||
# "model_post_init", "model_rebuild", "model_validate", "model_validate_json",
|
||||
# "model_validate_strings"
|
||||
_RESERVED_NAMES = {key for key in dir(BaseModel) if not key.startswith("_")}
|
||||
|
||||
|
||||
def _remap_field_definitions(field_definitions: dict[str, Any]) -> dict[str, Any]:
|
||||
"""This remaps fields to avoid colliding with internal pydantic fields."""
|
||||
|
||||
remapped = {}
|
||||
for key, value in field_definitions.items():
|
||||
if key.startswith("_") or key in _RESERVED_NAMES:
|
||||
# Let's add a prefix to avoid colliding with internal pydantic fields
|
||||
if isinstance(value, FieldInfo):
|
||||
msg = (
|
||||
f"Remapping for fields starting with '_' or fields with a name "
|
||||
f"matching a reserved name {_RESERVED_NAMES} is not supported if "
|
||||
f" the field is a pydantic Field instance. Got {key}."
|
||||
)
|
||||
raise NotImplementedError(msg)
|
||||
type_, default_ = value
|
||||
remapped[f"private_{key}"] = (
|
||||
type_,
|
||||
Field(
|
||||
default=default_,
|
||||
alias=key,
|
||||
serialization_alias=key,
|
||||
title=key.lstrip("_").replace("_", " ").title(),
|
||||
),
|
||||
)
|
||||
else:
|
||||
remapped[key] = value
|
||||
return remapped
|
||||
|
||||
|
||||
def create_model(
|
||||
@@ -15,19 +186,67 @@ def create_model(
|
||||
) -> type[BaseModel]:
|
||||
"""Create a pydantic model with the given field definitions.
|
||||
|
||||
Attention:
|
||||
Please do not use outside of langchain packages. This API
|
||||
is subject to change at any time.
|
||||
|
||||
Args:
|
||||
model_name: The name of the model.
|
||||
module_name: The name of the module where the model is defined.
|
||||
This is used by Pydantic to resolve any forward references.
|
||||
field_definitions: The field definitions for the model.
|
||||
root: Type for a root model (RootModel)
|
||||
"""
|
||||
# for langchain-core >= 0.3.0
|
||||
from langchain_core.utils.pydantic import create_model_v2
|
||||
|
||||
return create_model_v2(
|
||||
model_name,
|
||||
field_definitions=field_definitions,
|
||||
root=root,
|
||||
)
|
||||
Returns:
|
||||
Type[BaseModel]: The created model.
|
||||
"""
|
||||
field_definitions = field_definitions or {}
|
||||
|
||||
if root:
|
||||
if field_definitions:
|
||||
msg = (
|
||||
"When specifying __root__ no other "
|
||||
f"fields should be provided. Got {field_definitions}"
|
||||
)
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
if isinstance(root, tuple):
|
||||
kwargs = {"type_": root[0], "default_": root[1]}
|
||||
else:
|
||||
kwargs = {"type_": root}
|
||||
|
||||
try:
|
||||
named_root_model = _create_root_model_cached(model_name, **kwargs)
|
||||
except TypeError:
|
||||
# something in the arguments into _create_root_model_cached is not hashable
|
||||
named_root_model = _create_root_model(
|
||||
model_name,
|
||||
**kwargs,
|
||||
)
|
||||
return named_root_model
|
||||
|
||||
# No root, just field definitions
|
||||
names = set(field_definitions.keys())
|
||||
|
||||
capture_warnings = False
|
||||
|
||||
for name in names:
|
||||
# Also if any non-reserved name is used (e.g., model_id or model_name)
|
||||
if name.startswith("model"):
|
||||
capture_warnings = True
|
||||
|
||||
with warnings.catch_warnings() if capture_warnings else nullcontext():
|
||||
if capture_warnings:
|
||||
warnings.filterwarnings(action="ignore")
|
||||
try:
|
||||
return _create_model_cached(model_name, **field_definitions)
|
||||
except TypeError:
|
||||
# something in field definitions is not hashable
|
||||
return _create_model_base(
|
||||
model_name,
|
||||
__config__=_SchemaConfig,
|
||||
**_remap_field_definitions(field_definitions),
|
||||
)
|
||||
|
||||
|
||||
def is_supported_by_pydantic(type_: Any) -> bool:
|
||||
@@ -45,7 +264,7 @@ def is_supported_by_pydantic(type_: Any) -> bool:
|
||||
|
||||
if hasattr(type_, "__orig_bases__"):
|
||||
for base in type_.__orig_bases__:
|
||||
if base is typing_extensions.TypedDict:
|
||||
if base is TypedDict:
|
||||
return True
|
||||
elif base is typing.TypedDict: # noqa: TID251
|
||||
# ignoring TID251 since it's OK to use typing.TypedDict in this case.
|
||||
|
||||
Generated
+1
-5
@@ -26,7 +26,6 @@ description = "Reusable constraint types to use with typing.Annotated"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||
@@ -2270,7 +2269,6 @@ description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"},
|
||||
{file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"},
|
||||
@@ -2295,7 +2293,6 @@ description = "Core functionality for Pydantic validation and serialization"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"},
|
||||
{file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"},
|
||||
@@ -3306,7 +3303,6 @@ files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
]
|
||||
markers = {main = "python_version < \"4.0\""}
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
@@ -3677,4 +3673,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.9"
|
||||
content-hash = "0e0c3fc2d5a5348c8df102497c7221a1f052f37a788b315eb82dfc7d63423e17"
|
||||
content-hash = "770dcaa5816fffb667b5e3639c0ee5add24df0a99bb9a89a6d8fb2c4a79fb185"
|
||||
|
||||
@@ -14,6 +14,7 @@ langgraph-checkpoint = "^2.0.10"
|
||||
langgraph-sdk = { version = ">=0.1.42", python = "<4.0" }
|
||||
langgraph-prebuilt = { version = ">=0.1.8", python = "<4.0" }
|
||||
xxhash = "^3.5.0"
|
||||
pydantic = { version = ">=2.7.4"}
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^8.3.2"
|
||||
|
||||
Reference in New Issue
Block a user