mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-13 13:17:52 +02:00
fix(langgraph): check langgraph-api version for DeltaChannel support
Replace the checkpointer-capability heuristic with a direct `langgraph-api` version check: `DeltaChannel` reconstruction needs server-side support added in `langgraph-api>0.9.0`. `StateGraph.compile` now raises a `RuntimeError` when a delta-channel graph is compiled under an older API server, and is a no-op when `langgraph-api` is not installed (local execution). Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
co-authored by
open-swe[bot] <open-swe@users.noreply.github.com>
parent
4e4bf1a204
commit
9752282073
@@ -9,6 +9,7 @@ from collections.abc import Awaitable, Callable, Hashable, Mapping, Sequence
|
||||
from dataclasses import dataclass, is_dataclass
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
from importlib import metadata
|
||||
from inspect import isclass, isfunction, ismethod, signature
|
||||
from types import FunctionType
|
||||
from types import NoneType as NoneType
|
||||
@@ -27,8 +28,9 @@ from typing import (
|
||||
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver, Checkpoint
|
||||
from langgraph.checkpoint.base import Checkpoint
|
||||
from langgraph.store.base import BaseStore
|
||||
from packaging.version import Version
|
||||
from pydantic import BaseModel, TypeAdapter
|
||||
from typing_extensions import NotRequired, Required, Self, Unpack, is_typeddict
|
||||
|
||||
@@ -120,32 +122,31 @@ def _warn_invalid_state_schema(schema: type[Any] | Any) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _warn_if_checkpointer_lacks_delta_support(
|
||||
channels: Mapping[str, Any], checkpointer: Checkpointer
|
||||
) -> None:
|
||||
"""Warn when a `DeltaChannel` graph has a checkpointer that can't reconstruct it.
|
||||
_MIN_DELTA_CHANNEL_API_VERSION = Version("0.9.0")
|
||||
|
||||
`DeltaChannel` stores only a sentinel in checkpoint blobs and rebuilds
|
||||
state by replaying ancestor writes via `BaseCheckpointSaver`'s delta
|
||||
history API (`get_delta_channel_history`, added in
|
||||
`langgraph-checkpoint>=4.1.0`). A saver from an older `langgraph-checkpoint`
|
||||
lacks that method, so reconstruction fails at runtime. Warn at compile
|
||||
time so users know to upgrade.
|
||||
|
||||
def _check_delta_channel_api_support(channels: Mapping[str, Any]) -> None:
|
||||
"""Raise if a `DeltaChannel` graph runs under an API server too old to support it.
|
||||
|
||||
`DeltaChannel` reconstruction depends on server-side support added in
|
||||
`langgraph-api>0.9.0`. When running under an older API server, delta
|
||||
channels fail at runtime, so we raise at compile time with an upgrade
|
||||
hint. The check is skipped when `langgraph-api` is not installed (e.g.
|
||||
local execution), since there is no API server to be incompatible.
|
||||
"""
|
||||
if not isinstance(checkpointer, BaseCheckpointSaver):
|
||||
return
|
||||
if not any(isinstance(c, DeltaChannel) for c in channels.values()):
|
||||
return
|
||||
if hasattr(checkpointer, "get_delta_channel_history"):
|
||||
try:
|
||||
api_version = metadata.version("langgraph-api")
|
||||
except metadata.PackageNotFoundError:
|
||||
return
|
||||
warnings.warn(
|
||||
f"The configured checkpointer ({type(checkpointer).__name__}) does not "
|
||||
"support `DeltaChannel` state reconstruction, which requires the "
|
||||
"`get_delta_channel_history` API added in `langgraph-checkpoint>=4.1.0`. "
|
||||
"State reconstruction will fail at runtime. Upgrade `langgraph-checkpoint` "
|
||||
"(e.g. `pip install -U langgraph-checkpoint`) to enable `DeltaChannel` support.",
|
||||
UserWarning,
|
||||
stacklevel=4,
|
||||
if Version(api_version) > _MIN_DELTA_CHANNEL_API_VERSION:
|
||||
return
|
||||
raise RuntimeError(
|
||||
f"`DeltaChannel` requires `langgraph-api>0.9.0`, but the installed "
|
||||
f"version is {api_version}. Upgrade `langgraph-api` "
|
||||
"(e.g. `pip install -U langgraph-api`) to use graphs that rely on "
|
||||
"`DeltaChannel`."
|
||||
)
|
||||
|
||||
|
||||
@@ -1245,7 +1246,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
CompiledStateGraph: The compiled `StateGraph`.
|
||||
"""
|
||||
checkpointer = ensure_valid_checkpointer(checkpointer)
|
||||
_warn_if_checkpointer_lacks_delta_support(self.channels, checkpointer)
|
||||
_check_delta_channel_api_support(self.channels)
|
||||
|
||||
serde_allowlist: set[tuple[str, ...]] | None = None
|
||||
if _serde.STRICT_MSGPACK_ENABLED:
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Compile-time check that `DeltaChannel` graphs run under a supported API server.
|
||||
|
||||
`DeltaChannel` reconstruction relies on server-side support added in
|
||||
`langgraph-api>0.9.0`. When running under an older API server, delta channels
|
||||
fail at runtime, so `StateGraph.compile` raises with an upgrade hint. The check
|
||||
is skipped when `langgraph-api` is not installed (local execution).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from typing import Annotated
|
||||
|
||||
import pytest
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.channels.delta import DeltaChannel
|
||||
from langgraph.constants import START
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.graph import state as state_module
|
||||
from langgraph.graph.message import _messages_delta_reducer
|
||||
|
||||
|
||||
class _DeltaState(TypedDict):
|
||||
messages: Annotated[
|
||||
list, DeltaChannel(_messages_delta_reducer, snapshot_frequency=50)
|
||||
]
|
||||
|
||||
|
||||
class _PlainState(TypedDict):
|
||||
values: Annotated[list, lambda a, b: (a or []) + (b or [])]
|
||||
|
||||
|
||||
def _delta_graph() -> StateGraph:
|
||||
return (
|
||||
StateGraph(_DeltaState)
|
||||
.add_node("n", lambda s: {"messages": []})
|
||||
.add_edge(START, "n")
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api_version(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Override the reported `langgraph-api` version (or simulate absence)."""
|
||||
|
||||
def _set(version: str | None) -> None:
|
||||
def fake_version(name: str) -> str:
|
||||
if name == "langgraph-api":
|
||||
if version is None:
|
||||
raise state_module.metadata.PackageNotFoundError(name)
|
||||
return version
|
||||
return state_module.metadata.version(name)
|
||||
|
||||
monkeypatch.setattr(state_module.metadata, "version", fake_version)
|
||||
|
||||
return _set
|
||||
|
||||
|
||||
def test_delta_graph_raises_for_old_api(api_version) -> None:
|
||||
api_version("0.9.0")
|
||||
with pytest.raises(RuntimeError, match="requires `langgraph-api>0.9.0`"):
|
||||
_delta_graph().compile()
|
||||
|
||||
|
||||
def test_delta_graph_ok_for_new_api(api_version) -> None:
|
||||
api_version("0.9.1")
|
||||
_delta_graph().compile()
|
||||
|
||||
|
||||
def test_delta_graph_ok_for_much_newer_api(api_version) -> None:
|
||||
api_version("0.11.2")
|
||||
_delta_graph().compile()
|
||||
|
||||
|
||||
def test_delta_graph_ok_when_api_not_installed(api_version) -> None:
|
||||
api_version(None)
|
||||
_delta_graph().compile()
|
||||
|
||||
|
||||
def test_non_delta_graph_never_raises(api_version) -> None:
|
||||
api_version("0.1.0")
|
||||
graph = (
|
||||
StateGraph(_PlainState)
|
||||
.add_node("n", lambda s: {"values": []})
|
||||
.add_edge(START, "n")
|
||||
)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
graph.compile()
|
||||
@@ -1,84 +0,0 @@
|
||||
"""Compile-time warning when a checkpointer can't reconstruct `DeltaChannel`.
|
||||
|
||||
`DeltaChannel` reconstructs state via `BaseCheckpointSaver`'s delta history
|
||||
API (`get_delta_channel_history`, added in `langgraph-checkpoint>=4.1.0`). A
|
||||
saver from an older package lacks that method, so reconstruction fails at
|
||||
runtime. `StateGraph.compile` warns at compile time so users upgrade.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from typing import Annotated
|
||||
|
||||
import pytest
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.channels.delta import DeltaChannel
|
||||
from langgraph.constants import START
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.graph.message import _messages_delta_reducer
|
||||
|
||||
|
||||
class _DeltaState(TypedDict):
|
||||
messages: Annotated[
|
||||
list, DeltaChannel(_messages_delta_reducer, snapshot_frequency=50)
|
||||
]
|
||||
|
||||
|
||||
class _PlainState(TypedDict):
|
||||
values: Annotated[list, lambda a, b: (a or []) + (b or [])]
|
||||
|
||||
|
||||
def _delta_graph() -> StateGraph:
|
||||
return (
|
||||
StateGraph(_DeltaState)
|
||||
.add_node("n", lambda s: {"messages": []})
|
||||
.add_edge(START, "n")
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def saver_without_delta_api(monkeypatch: pytest.MonkeyPatch) -> InMemorySaver:
|
||||
"""A saver whose class tree lacks the delta history API (old package)."""
|
||||
for cls in (InMemorySaver, BaseCheckpointSaver):
|
||||
for name in ("get_delta_channel_history", "aget_delta_channel_history"):
|
||||
if name in cls.__dict__:
|
||||
monkeypatch.delattr(cls, name)
|
||||
saver = InMemorySaver()
|
||||
assert not hasattr(saver, "get_delta_channel_history")
|
||||
return saver
|
||||
|
||||
|
||||
def test_delta_graph_warns_for_unsupported_checkpointer(
|
||||
saver_without_delta_api: InMemorySaver,
|
||||
) -> None:
|
||||
with pytest.warns(UserWarning, match="does not support `DeltaChannel`"):
|
||||
_delta_graph().compile(checkpointer=saver_without_delta_api)
|
||||
|
||||
|
||||
def test_delta_graph_no_warning_for_supported_checkpointer() -> None:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
_delta_graph().compile(checkpointer=InMemorySaver())
|
||||
|
||||
|
||||
def test_delta_graph_no_warning_without_checkpointer() -> None:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
_delta_graph().compile()
|
||||
|
||||
|
||||
def test_non_delta_graph_no_warning_with_unsupported_checkpointer(
|
||||
saver_without_delta_api: InMemorySaver,
|
||||
) -> None:
|
||||
graph = (
|
||||
StateGraph(_PlainState)
|
||||
.add_node("n", lambda s: {"values": []})
|
||||
.add_edge(START, "n")
|
||||
)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
graph.compile(checkpointer=saver_without_delta_api)
|
||||
Reference in New Issue
Block a user