Compare commits

...

19 Commits

Author SHA1 Message Date
Eugene Yurtsev
dd9103175f x 2024-03-15 13:14:34 -04:00
Eugene Yurtsev
42b680d5fa x 2024-03-15 13:02:01 -04:00
Eugene Yurtsev
1d1b6f22db x 2024-03-15 13:01:58 -04:00
Eugene Yurtsev
0111ba6d27 x 2024-03-15 11:53:28 -04:00
Eugene Yurtsev
9a5b641d42 x 2024-03-15 11:52:18 -04:00
Eugene Yurtsev
e661e2574e x 2024-03-15 11:45:20 -04:00
Eugene Yurtsev
505f1bf3d9 x 2024-03-15 11:43:21 -04:00
Eugene Yurtsev
cf4d7fb64e x 2024-03-15 11:37:35 -04:00
Eugene Yurtsev
b4f10e0080 x 2024-03-15 11:34:19 -04:00
Eugene Yurtsev
a6158d8d20 x 2024-03-15 11:25:51 -04:00
Eugene Yurtsev
07e3c3594e x 2024-03-15 11:24:30 -04:00
Eugene Yurtsev
01400705f0 x 2024-03-15 10:58:31 -04:00
Eugene Yurtsev
5762c4b63a x 2024-03-15 10:25:55 -04:00
Eugene Yurtsev
4b519883eb x 2024-03-15 09:28:16 -04:00
Eugene Yurtsev
7f3d999e1e x 2024-03-15 09:27:56 -04:00
Eugene Yurtsev
4cc21c404e x 2024-03-14 17:59:42 -04:00
Eugene Yurtsev
dc9f2e4174 x 2024-03-14 17:54:55 -04:00
Eugene Yurtsev
610a2e07b8 x 2024-03-14 17:44:39 -04:00
Eugene Yurtsev
54e40830f9 x 2024-03-14 17:28:57 -04:00
16 changed files with 237 additions and 28 deletions

View File

@@ -1,3 +1,12 @@
# The dependencies action runs unit tests with different versions of the package
# dependencies.
# This is our current solution for setting up a test matrix primarily for
# testing compatibility with different versions of pydantic.
# This code has some duplication with the _test action due to the need to
# do extra logic if pydantic version 2 is installed; i.e., maybe run
# with LC_PYDANTIC_V2_EXPERIMENTAL=True
# The extra logic may not be worth refactoring as it'll be deleted once
# migration to pydantic v2 proper is complete.
name: dependencies
on:
@@ -103,7 +112,27 @@ jobs:
if: ${{ !startsWith(inputs.working-directory, 'libs/partners/airbyte') }}
shell: bash
run: make test
- name: Maybe run unit tests with LC_PYDANTIC_V2_EXPERIMENTAL=True
# This step will run unit tests if pydantic>=2.0.0 is installed
# It will run unit tests with the environment variable LC_PYDANTIC_V2_EXPERIMENTAL=True
# libraries that support the flag, will attempt to run unit tests
# using pydantic proper rather than via the pydantic.v1 namespace.
# TODO: Refactor how code is tested with LC_PYDANTIC_V2_EXPERIMENTAL=True
# We should probably have a JSON file that lists which code paths
# that support the flag
if: ${{ !startsWith(inputs.working-directory, 'libs/core/') }}
shell: bash
run: |
# Determine the major part of pydantic version
REGULAR_VERSION=$(poetry run python -c "import pydantic; print(pydantic.__version__)" | cut -d. -f1)
# If version 2 then we run tests with LC_PYDANTIC_V2_EXPERIMENTAL=True
# Otherwise echo that there's nothing to do since the version is 1
if [[ "$REGULAR_VERSION" == "2" ]]; then
LC_PYDANTIC_V2_EXPERIMENTAL=True make test
echo "Finished running unit tests with LC_PYDANTIC_V2_EXPERIMENTAL=True"
else
echo "No running tests with LC_PYDANTIC_V2_EXPERIMENTAL=True since pydantic version is 1"
fi
- name: Ensure the tests did not create any additional files
shell: bash
run: |

View File

@@ -57,6 +57,27 @@ jobs:
run: |
make test
- name: Maybe run unit tests with LC_PYDANTIC_V2_EXPERIMENTAL=True
# This step will run unit tests if pydantic>=2.0.0 is installed
# It will run unit tests with the environment variable LC_PYDANTIC_V2_EXPERIMENTAL=True
# libraries that support the flag, will attempt to run unit tests
# using pydantic proper rather than via the pydantic.v1 namespace.
# TODO: Refactor how code is tested with LC_PYDANTIC_V2_EXPERIMENTAL=True
# We should probably have a JSON file that lists which code paths
# that support the flag
if: ${{ !startsWith(inputs.working-directory, 'libs/core/') }}
shell: bash
run: |
# Determine the major part of pydantic version
REGULAR_VERSION=$(poetry run python -c "import pydantic; print(pydantic.__version__)" | cut -d. -f1)
# If version 2 then we run tests with LC_PYDANTIC_V2_EXPERIMENTAL=True
# Otherwise echo that there's nothing to do since the version is 1
if [[ "$REGULAR_VERSION" == "2" ]]; then
LC_PYDANTIC_V2_EXPERIMENTAL=True make test
echo "Finished running unit tests with LC_PYDANTIC_V2_EXPERIMENTAL=True"
else
echo "No running tests with LC_PYDANTIC_V2_EXPERIMENTAL=True since pydantic version is 1"
fi
- name: Ensure the tests did not create any additional files
shell: bash
run: |

View File

@@ -5,7 +5,7 @@ from typing import Optional, Sequence
from langchain_core.callbacks import Callbacks
from langchain_core.documents import Document
from langchain_core.pydantic_v1 import BaseModel
from langchain_core.pydantic import BaseModel
from langchain_core.runnables import run_in_executor

View File

@@ -26,7 +26,8 @@ from langchain_core.messages import (
get_buffer_string,
)
from langchain_core.prompt_values import PromptValue
from langchain_core.pydantic_v1 import BaseModel, Field, validator
from langchain_core.pydantic import BaseModel, Field
from langchain_core.pydantic_v1 import validator
from langchain_core.runnables import Runnable, RunnableSerializable
from langchain_core.utils import get_pydantic_field_names

View File

@@ -12,7 +12,7 @@ from typing import (
from typing_extensions import NotRequired
from langchain_core.pydantic_v1 import BaseModel, PrivateAttr
from langchain_core.pydantic import BaseModel, PrivateAttr
class BaseSerialized(TypedDict):
@@ -163,7 +163,7 @@ class Serializable(BaseModel, ABC):
for key in list(secrets):
value = secrets[key]
if key in this.__fields__:
secrets[this.__fields__[key].alias] = value
secrets[this.__fields__[key].alias] = value # type: ignore[index]
lc_kwargs.update(this.lc_attributes)
# include all secrets, even if not specified in kwargs

View File

@@ -1,10 +1,9 @@
from __future__ import annotations
from typing import Any, Dict, List, Literal
from typing import Any, List, Literal
from langchain_core.messages import BaseMessage, BaseMessageChunk
from langchain_core.outputs.generation import Generation
from langchain_core.pydantic_v1 import root_validator
from langchain_core.utils._merge import merge_dicts
@@ -12,21 +11,19 @@ class ChatGeneration(Generation):
"""A single chat generation output."""
text: str = ""
"""*SHOULD NOT BE SET DIRECTLY* The text contents of the output message."""
# """*SHOULD NOT BE SET DIRECTLY* The text contents of the output message."""
message: BaseMessage
"""The message output by the chat model."""
# Override type to be ChatGeneration, ignore mypy error as this is intentional
type: Literal["ChatGeneration"] = "ChatGeneration" # type: ignore[assignment]
"""Type is used exclusively for serialization purposes."""
@root_validator
def set_text(cls, values: Dict[str, Any]) -> Dict[str, Any]:
"""Set the text attribute to be the contents of the message."""
try:
values["text"] = values["message"].content
except (KeyError, AttributeError) as e:
raise ValueError("Error while initializing ChatGeneration") from e
return values
def __init__(self, *, message: BaseMessage, **kwargs: Any) -> None:
"""Initialize a ChatGeneration object."""
# Backwards compatibility delete text if it provided.
# This would arise primarily from de-serialization of old objects.
kwargs["text"] = message.content
super().__init__(message=message, **kwargs) # type: ignore[call-arg]
@classmethod
def get_lc_namespace(cls) -> List[str]:

View File

@@ -25,7 +25,8 @@ from langchain_core.prompt_values import (
PromptValue,
StringPromptValue,
)
from langchain_core.pydantic_v1 import BaseModel, Field, root_validator
from langchain_core.pydantic import BaseModel, Field
from langchain_core.pydantic_v1 import root_validator
from langchain_core.runnables import RunnableConfig, RunnableSerializable
from langchain_core.runnables.config import ensure_config
from langchain_core.runnables.utils import create_model

View File

@@ -109,7 +109,7 @@ class MessagesPlaceholder(BaseMessagePromptTemplate):
return ["langchain", "prompts", "chat"]
def __init__(self, variable_name: str, *, optional: bool = False, **kwargs: Any):
super().__init__(variable_name=variable_name, optional=optional, **kwargs)
super().__init__(variable_name=variable_name, optional=optional, **kwargs) # type: ignore[call-arg]
def format_messages(self, **kwargs: Any) -> List[BaseMessage]:
"""Format messages from kwargs.

View File

@@ -0,0 +1,23 @@
from .config import _PYDANTIC_MAJOR_VERSION, _PYDANTIC_VERSION, USE_PYDANTIC_V2
# This is a compatibility layer that pydantic 2 proper, pydantic.v1 in pydantic 2,
# or pydantic 1
if USE_PYDANTIC_V2:
from pydantic import BaseModel, Field, PrivateAttr
else:
from langchain_core.pydantic_v1 import ( # type: ignore[no-redef]
BaseModel,
Field,
PrivateAttr,
)
# Only expose things that are common across all pydantic versions
__all__ = [ # noqa: F405
"BaseModel",
"Field",
"PrivateAttr",
"_PYDANTIC_MAJOR_VERSION",
"_PYDANTIC_VERSION",
"USE_PYDANTIC_V2",
]

View File

@@ -0,0 +1,30 @@
import os
from importlib import metadata
_PYDANTIC_VERSION = metadata.version("pydantic")
try:
_PYDANTIC_MAJOR_VERSION: int = int(_PYDANTIC_VERSION.split(".")[0])
except metadata.PackageNotFoundError:
_PYDANTIC_MAJOR_VERSION = 0
def _get_use_pydantic_v2() -> bool:
"""Get the value of the LC_PYDANTIC_V2_EXPERIMENTAL environment variable."""
value = os.environ.get("LC_PYDANTIC_V2_EXPERIMENTAL", "false").lower()
if value == "true":
if _PYDANTIC_MAJOR_VERSION != 2:
raise ValueError(
f"LC_PYDANTIC_V2_EXPERIMENTAL is set to true, "
f"but pydantic version is {_PYDANTIC_VERSION}"
)
return True
elif value == "false":
return False
else:
raise ValueError(
f"Invalid value for LANGCHAIN_PYDANTIC_V2_EXPERIMENTAL: {value}"
)
USE_PYDANTIC_V2 = _get_use_pydantic_v2()

View File

@@ -1,3 +1,4 @@
# type: ignore
from importlib import metadata
## Create namespaces for pydantic v1 and v2.
@@ -12,12 +13,45 @@ from importlib import metadata
# * This change is easier to roll out and roll back.
try:
from pydantic.v1 import * # noqa: F403 # type: ignore
from pydantic.v1 import ( # noqa: F403 # type: ignore
BaseModel,
Extra,
Field,
PrivateAttr,
SecretStr,
ValidationError,
create_model,
root_validator,
validate_arguments,
)
except ImportError:
from pydantic import * # noqa: F403 # type: ignore
from pydantic import ( # noqa: F403 # type: ignore
BaseModel,
Extra,
Field,
PrivateAttr,
SecretStr,
ValidationError,
create_model,
root_validator,
validate_arguments,
)
try:
_PYDANTIC_MAJOR_VERSION: int = int(metadata.version("pydantic").split(".")[0])
except metadata.PackageNotFoundError:
_PYDANTIC_MAJOR_VERSION = 0
__all__ = [
"BaseModel",
"Field",
"PrivateAttr",
"SecretStr",
"_PYDANTIC_MAJOR_VERSION",
"Extra",
"ValidationError",
"root_validator",
"validate_arguments",
"create_model",
]

View File

@@ -237,7 +237,7 @@ class RunnableConfigurableFields(DynamicRunnable[Input, Output]):
id=spec.id,
name=spec.name,
description=spec.description
or self.default.__fields__[field_name].field_info.description,
or self.default.__fields__[field_name].field_info.description, # type: ignore[attr-defined]
annotation=spec.annotation
or self.default.__fields__[field_name].annotation,
default=getattr(self.default, field_name),
@@ -245,7 +245,8 @@ class RunnableConfigurableFields(DynamicRunnable[Input, Output]):
)
if isinstance(spec, ConfigurableField)
else make_options_spec(
spec, self.default.__fields__[field_name].field_info.description
spec,
self.default.__fields__[field_name].field_info.description, # type: ignore[attr-defined]
)
for field_name, spec in self.fields.items()
]

View File

@@ -34,10 +34,9 @@ from langchain_core.callbacks import (
Callbacks,
)
from langchain_core.load.serializable import Serializable
from langchain_core.pydantic import BaseModel, Field
from langchain_core.pydantic_v1 import (
BaseModel,
Extra,
Field,
ValidationError,
create_model,
root_validator,
@@ -62,7 +61,7 @@ def _create_subset_model(
"""Create a pydantic model with only a subset of model's fields."""
fields = {}
for field_name in field_names:
field = model.__fields__[field_name]
field = model.__fields__[field_name] # type: ignore[index]
t = (
# this isn't perfect but should work for most functions
field.outer_type_
@@ -272,7 +271,7 @@ class ChildTool(BaseTool):
input_args = self.args_schema
if isinstance(tool_input, str):
if input_args is not None:
key_ = next(iter(input_args.__fields__.keys()))
key_ = next(iter(input_args.__fields__.keys())) # type: ignore[attr-defined]
input_args.validate({key_: tool_input})
return tool_input
else:

View File

@@ -330,7 +330,7 @@ class BaseTracer(BaseCallbackHandler, ABC):
llm_run.outputs = response.dict()
for i, generations in enumerate(response.generations):
for j, generation in enumerate(generations):
output_generation = llm_run.outputs["generations"][i][j]
output_generation = llm_run.outputs["generations"][i][j] # type: ignore[index]
if "message" in output_generation:
output_generation["message"] = dumpd(
cast(ChatGeneration, generation).message

View File

@@ -81,6 +81,11 @@ select = [
disallow_untyped_defs = "True"
exclude = ["notebooks", "examples", "example_data", "langchain_core/pydantic"]
[[tool.mypy.overrides]]
module = "langchain_core.pydantic_v1"
follow_imports = "skip"
ignore_errors = true
[tool.coverage.run]
omit = ["tests/*"]

View File

@@ -3,7 +3,16 @@ from importlib import util
from typing import Dict, Sequence
import pytest
from pytest import Config, Function, Parser
from _pytest.config import Config
from _pytest.terminal import TerminalReporter
from pytest import Function, Parser
from langchain_core.pydantic import _PYDANTIC_VERSION
from langchain_core.pydantic.config import USE_PYDANTIC_V2
# The maximum number of failed tests to allow when running with
# This number should only be decreased over time until we're at 0!
MAX_FAILED_LC_PYDANTIC_2_MIGRATION = 100
def pytest_addoption(parser: Parser) -> None:
@@ -18,6 +27,65 @@ def pytest_addoption(parser: Parser) -> None:
action="store_true",
help="Only run core tests. Never runs any extended tests.",
)
parser.addoption(
"--max-fail",
type=int,
default=0,
help="Maximum number of failed tests to allow. "
"Should only be set for LC_PYDANTIC_V2_EXPERIMENTAL=true.",
)
def pytest_sessionstart(session: pytest.Session) -> None:
"""Initialize the count of passed and failed tests."""
session.count_failed = 0 # type: ignore[attr-defined]
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo) -> None: # type: ignore
outcome = yield
result = outcome.get_result()
if result.when == "call" and result.failed:
item.session.count_failed += 1 # type: ignore[attr-defined]
def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
"""Exit with a non-zero status if not enough tests pass."""
max_fail = session.config.getoption(
"--max-fail", default=MAX_FAILED_LC_PYDANTIC_2_MIGRATION
)
if max_fail > 0 and not USE_PYDANTIC_V2:
raise ValueError(
"The `--max-fail` option should only be set when "
"running with `LC_PYDANTIC_V2_EXPERIMENTAL=true`."
)
# This will set up a ratchet approach so that the number of failures
# has to go down over time.
if session.count_failed > max_fail: # type: ignore[attr-defined]
session.exitstatus = 1
reporter = session.config.pluginmanager.get_plugin("terminalreporter")
reporter.section("Session errors", sep="-", red=True, bold=True) # type: ignore[union-attr]
reporter.line( # type: ignore[union-attr]
f"Regression in pydantic v2 migration. Expected at most {max_fail} failed "
f"tests. Instead found {session.count_failed} failed tests." # type: ignore[attr-defined]
)
else:
session.exitstatus = 0
def pytest_terminal_summary(
terminalreporter: TerminalReporter, exitstatus: int, config: Config
) -> None:
"""Add custom information to the terminal summary."""
terminalreporter.write_sep("-", title="Pydantic Configuration")
terminalreporter.write_line(f"Testing with pydantic version {_PYDANTIC_VERSION}.")
# Let's print out the value of USE_PYDANTIC_V2
terminalreporter.write_line(
f"USE_PYDANTIC_V2: {USE_PYDANTIC_V2}. "
f"Enable with `LC_PYDANTIC_V2_EXPERIMENTAL=true` env variable "
f"and pydantic>=2 installed."
)
def pytest_collection_modifyitems(config: Config, items: Sequence[Function]) -> None: