fix(langchain-classic): fix Chain.save() regression from dict-to-model_dump migration (#35667)

Fixes #35665

---

Fixes a regression introduced in #33035 where `Chain.save()` stopped
working for chain types that support saving, including `LLMChain`.

`Chain.save()` now calls `self.model_dump()`, but the `_type` injection
still lived in the deprecated `dict()` override. As a result, serialized
chains no longer included `_type`, and `save()` always raised the "does
not support saving" error.

This moves the override to `model_dump()` so saved chain output includes
`_type` again. Pydantic v2's `dict()` method delegates to
`model_dump()`, so existing `dict()` callers continue to get the same
serialized shape.
This commit is contained in:
Mohammad Mohtashim
2026-07-06 21:15:21 +05:00
committed by GitHub
parent b08c3913fb
commit 2d8100c4fa
2 changed files with 60 additions and 3 deletions

View File

@@ -732,15 +732,15 @@ class Chain(RunnableSerializable[dict[str, Any], dict[str, Any]], ABC):
)
raise ValueError(msg)
def dict(self, **kwargs: Any) -> dict:
def model_dump(self, **kwargs: Any) -> dict:
"""Dictionary representation of chain.
Expects `Chain._chain_type` property to be implemented and for memory to be
null.
Args:
**kwargs: Keyword arguments passed to default `pydantic.BaseModel.dict`
method.
**kwargs: Keyword arguments passed to default
`pydantic.BaseModel.model_dump` method.
Returns:
A dictionary representation of the chain.

View File

@@ -2,6 +2,7 @@
import re
import uuid
from pathlib import Path
from typing import Any
import pytest
@@ -66,6 +67,14 @@ class FakeChain(Chain):
return {"baz": "bar"}
class FakeSavableChain(FakeChain):
"""Fake chain that supports saving via _chain_type."""
@property
def _chain_type(self) -> str:
return "fake_savable"
def test_bad_inputs() -> None:
"""Test errors are raised if input keys are not found."""
chain = FakeChain()
@@ -243,3 +252,51 @@ def test_run_with_callback_and_output_error() -> None:
assert handler.starts == 1
assert handler.ends == 0
assert handler.errors == 1
def test_model_dump_includes_type() -> None:
"""Test that model_dump includes _type when _chain_type is implemented."""
chain = FakeSavableChain()
dumped = chain.model_dump()
assert "_type" in dumped
assert dumped["_type"] == "fake_savable"
def test_model_dump_excludes_type_when_not_implemented() -> None:
"""Test that model_dump omits _type when _chain_type raises."""
chain = FakeChain()
dumped = chain.model_dump()
assert "_type" not in dumped
def test_save_yaml(tmp_path: Path) -> None:
"""Test that save() works for a chain that implements _chain_type."""
chain = FakeSavableChain()
file_path = tmp_path / "chain.yaml"
chain.save(str(file_path))
assert file_path.exists()
import yaml
with file_path.open() as f:
data = yaml.safe_load(f)
assert data["_type"] == "fake_savable"
def test_save_json(tmp_path: Path) -> None:
"""Test that save() works with JSON format."""
chain = FakeSavableChain()
file_path = tmp_path / "chain.json"
chain.save(str(file_path))
assert file_path.exists()
import json
with file_path.open() as f:
data = json.load(f)
assert data["_type"] == "fake_savable"
def test_save_raises_when_chain_type_not_implemented(tmp_path: Path) -> None:
"""Test that save() raises NotImplementedError for unsavable chains."""
chain = FakeChain()
with pytest.raises(NotImplementedError, match="does not support saving"):
chain.save(str(tmp_path / "chain.yaml"))