mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-12 12:47:53 +02:00
Add fast path to serialize None values (#4103)
- If the value to serialize is None we can use encode it in the string type, and skip msgpack encoding - Use None value for edge/branch channels in StateGraph
This commit is contained in:
@@ -30,6 +30,7 @@ from langgraph.checkpoint.serde.types import SendProtocol
|
||||
from langgraph.store.base import Item
|
||||
|
||||
LC_REVIVER = Reviver()
|
||||
EMPTY_BYTES = b""
|
||||
|
||||
|
||||
class JsonPlusSerializer(SerializerProtocol):
|
||||
@@ -194,7 +195,9 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
)
|
||||
|
||||
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
|
||||
if isinstance(obj, bytes):
|
||||
if obj is None:
|
||||
return "null", EMPTY_BYTES
|
||||
elif isinstance(obj, bytes):
|
||||
return "bytes", obj
|
||||
elif isinstance(obj, bytearray):
|
||||
return "bytearray", obj
|
||||
@@ -211,7 +214,9 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
|
||||
def loads_typed(self, data: tuple[str, bytes]) -> Any:
|
||||
type_, data_ = data
|
||||
if type_ == "bytes":
|
||||
if type_ == "null":
|
||||
return None
|
||||
elif type_ == "bytes":
|
||||
return data_
|
||||
elif type_ == "bytearray":
|
||||
return bytearray(data_)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Generic, Optional, Sequence, Type
|
||||
from typing import Any, Generic, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -30,10 +30,10 @@ class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def from_checkpoint(self, checkpoint: Optional[Value]) -> Self:
|
||||
def from_checkpoint(self, checkpoint: Value) -> Self:
|
||||
empty = self.__class__(self.typ)
|
||||
empty.key = self.key
|
||||
if checkpoint is not None:
|
||||
if checkpoint is not MISSING:
|
||||
empty.value = checkpoint
|
||||
return empty
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Generic, Optional, Sequence, TypeVar
|
||||
from typing import Any, Generic, Sequence, TypeVar
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -29,14 +29,14 @@ class BaseChannel(Generic[Value, Update, C], ABC):
|
||||
|
||||
# serialize/deserialize methods
|
||||
|
||||
def checkpoint(self) -> Optional[C]:
|
||||
def checkpoint(self) -> C:
|
||||
"""Return a serializable representation of the channel's current state.
|
||||
Raises EmptyChannelError if the channel is empty (never updated yet),
|
||||
or doesn't support checkpoints."""
|
||||
return self.get()
|
||||
|
||||
@abstractmethod
|
||||
def from_checkpoint(self, checkpoint: Optional[C]) -> Self:
|
||||
def from_checkpoint(self, checkpoint: C) -> Self:
|
||||
"""Return a new identical channel, optionally initialized from a checkpoint.
|
||||
If the checkpoint contains complex data structures, they should be copied."""
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import collections.abc
|
||||
from typing import (
|
||||
Callable,
|
||||
Generic,
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
)
|
||||
from typing import Callable, Generic, Sequence, Type
|
||||
|
||||
from typing_extensions import NotRequired, Required, Self
|
||||
|
||||
@@ -72,10 +66,10 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def from_checkpoint(self, checkpoint: Optional[Value]) -> Self:
|
||||
def from_checkpoint(self, checkpoint: Value) -> Self:
|
||||
empty = self.__class__(self.typ, self.operator)
|
||||
empty.key = self.key
|
||||
if checkpoint is not None:
|
||||
if checkpoint is not MISSING:
|
||||
empty.value = checkpoint
|
||||
return empty
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any, Generic, NamedTuple, Optional, Sequence, Type, Union
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
|
||||
|
||||
@@ -49,12 +50,11 @@ class DynamicBarrierValue(
|
||||
return (self.names, self.seen)
|
||||
|
||||
def from_checkpoint(
|
||||
self,
|
||||
checkpoint: Optional[tuple[Optional[set[Value]], set[Value]]],
|
||||
self, checkpoint: tuple[Optional[set[Value]], set[Value]]
|
||||
) -> Self:
|
||||
empty = self.__class__(self.typ)
|
||||
empty.key = self.key
|
||||
if checkpoint is not None:
|
||||
if checkpoint is not MISSING:
|
||||
names, seen = checkpoint
|
||||
empty.names = names if names is not None else None
|
||||
empty.seen = seen
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Generic, Optional, Sequence, Type
|
||||
from typing import Any, Generic, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -30,10 +30,10 @@ class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def from_checkpoint(self, checkpoint: Optional[Value]) -> Self:
|
||||
def from_checkpoint(self, checkpoint: Value) -> Self:
|
||||
empty = self.__class__(self.typ, self.guard)
|
||||
empty.key = self.key
|
||||
if checkpoint is not None:
|
||||
if checkpoint is not MISSING:
|
||||
empty.value = checkpoint
|
||||
return empty
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Generic, Optional, Sequence, Type
|
||||
from typing import Any, Generic, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -34,10 +34,10 @@ class LastValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def from_checkpoint(self, checkpoint: Optional[Value]) -> Self:
|
||||
def from_checkpoint(self, checkpoint: Value) -> Self:
|
||||
empty = self.__class__(self.typ)
|
||||
empty.key = self.key
|
||||
if checkpoint is not None:
|
||||
if checkpoint is not MISSING:
|
||||
empty.value = checkpoint
|
||||
return empty
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from typing import Generic, Optional, Sequence, Type
|
||||
from typing import Generic, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
|
||||
|
||||
@@ -35,10 +36,10 @@ class NamedBarrierValue(Generic[Value], BaseChannel[Value, Value, set[Value]]):
|
||||
def checkpoint(self) -> set[Value]:
|
||||
return self.seen
|
||||
|
||||
def from_checkpoint(self, checkpoint: Optional[set[Value]]) -> Self:
|
||||
def from_checkpoint(self, checkpoint: set[Value]) -> Self:
|
||||
empty = self.__class__(self.typ, self.names)
|
||||
empty.key = self.key
|
||||
if checkpoint is not None:
|
||||
if checkpoint is not MISSING:
|
||||
empty.seen = checkpoint
|
||||
return empty
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from typing import Any, Generic, Iterator, Optional, Sequence, Type, Union
|
||||
from typing import Any, Generic, Iterator, Sequence, Type, Union
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError
|
||||
|
||||
|
||||
@@ -16,9 +17,7 @@ def flatten(values: Sequence[Union[Value, list[Value]]]) -> Iterator[Value]:
|
||||
|
||||
class Topic(
|
||||
Generic[Value],
|
||||
BaseChannel[
|
||||
Sequence[Value], Union[Value, list[Value]], tuple[set[Value], list[Value]]
|
||||
],
|
||||
BaseChannel[Sequence[Value], Union[Value, list[Value]], list[Value]],
|
||||
):
|
||||
"""A configurable PubSub Topic.
|
||||
|
||||
@@ -49,14 +48,15 @@ class Topic(
|
||||
"""The type of the update received by the channel."""
|
||||
return Union[self.typ, list[self.typ]] # type: ignore[name-defined]
|
||||
|
||||
def checkpoint(self) -> tuple[set[Value], list[Value]]:
|
||||
def checkpoint(self) -> list[Value]:
|
||||
return self.values
|
||||
|
||||
def from_checkpoint(self, checkpoint: Optional[list[Value]]) -> Self:
|
||||
def from_checkpoint(self, checkpoint: list[Value]) -> Self:
|
||||
empty = self.__class__(self.typ, self.accumulate)
|
||||
empty.key = self.key
|
||||
if checkpoint is not None:
|
||||
if checkpoint is not MISSING:
|
||||
if isinstance(checkpoint, tuple):
|
||||
# backwards compatibility
|
||||
empty.values = checkpoint[1]
|
||||
else:
|
||||
empty.values = checkpoint
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Generic, Optional, Sequence, Type
|
||||
from typing import Generic, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -33,7 +33,7 @@ class UntrackedValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
def checkpoint(self) -> Value:
|
||||
raise EmptyChannelError()
|
||||
|
||||
def from_checkpoint(self, checkpoint: Optional[Value]) -> Self:
|
||||
def from_checkpoint(self, checkpoint: Value) -> Self:
|
||||
empty = self.__class__(self.typ, self.guard)
|
||||
empty.key = self.key
|
||||
return empty
|
||||
|
||||
@@ -845,7 +845,7 @@ class CompiledStateGraph(CompiledGraph):
|
||||
if end != END:
|
||||
self.nodes[starts].writers.append(
|
||||
ChannelWrite(
|
||||
(ChannelWriteEntry(CHANNEL_BRANCH_TO.format(end), starts),)
|
||||
(ChannelWriteEntry(CHANNEL_BRANCH_TO.format(end), None),)
|
||||
)
|
||||
)
|
||||
elif end != END:
|
||||
@@ -871,7 +871,7 @@ class CompiledStateGraph(CompiledGraph):
|
||||
if filtered := [p for p in packets if p != END]:
|
||||
writes = [
|
||||
(
|
||||
ChannelWriteEntry(CHANNEL_BRANCH_TO.format(p), start)
|
||||
ChannelWriteEntry(CHANNEL_BRANCH_TO.format(p), None)
|
||||
if not isinstance(p, Send)
|
||||
else p
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import AsyncIterator, Iterator, Mapping, Union
|
||||
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.checkpoint.base import Checkpoint
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.managed.base import (
|
||||
ConfiguredManagedValue,
|
||||
ManagedValueMapping,
|
||||
@@ -36,7 +37,7 @@ def ChannelsManager(
|
||||
with ExitStack() as stack:
|
||||
yield (
|
||||
{
|
||||
k: v.from_checkpoint(checkpoint["channel_values"].get(k))
|
||||
k: v.from_checkpoint(checkpoint["channel_values"].get(k, MISSING))
|
||||
for k, v in channel_specs.items()
|
||||
},
|
||||
ManagedValueMapping(
|
||||
@@ -90,7 +91,7 @@ async def AsyncChannelsManager(
|
||||
yield (
|
||||
# channels: enter each channel with checkpoint
|
||||
{
|
||||
k: v.from_checkpoint(checkpoint["channel_values"].get(k))
|
||||
k: v.from_checkpoint(checkpoint["channel_values"].get(k, MISSING))
|
||||
for k, v in channel_specs.items()
|
||||
},
|
||||
# managed: build mapping from spec to result
|
||||
|
||||
@@ -6,13 +6,14 @@ import pytest
|
||||
from langgraph.channels.binop import BinaryOperatorAggregate
|
||||
from langgraph.channels.last_value import LastValue
|
||||
from langgraph.channels.topic import Topic
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
def test_last_value() -> None:
|
||||
channel = LastValue(int).from_checkpoint(None)
|
||||
channel = LastValue(int).from_checkpoint(MISSING)
|
||||
assert channel.ValueType is int
|
||||
assert channel.UpdateType is int
|
||||
|
||||
@@ -31,7 +32,7 @@ def test_last_value() -> None:
|
||||
|
||||
|
||||
def test_topic() -> None:
|
||||
channel = Topic(str).from_checkpoint(None)
|
||||
channel = Topic(str).from_checkpoint(MISSING)
|
||||
assert channel.ValueType is Sequence[str]
|
||||
assert channel.UpdateType is Union[str, list[str]]
|
||||
|
||||
@@ -55,7 +56,7 @@ def test_topic() -> None:
|
||||
|
||||
|
||||
def test_topic_accumulate() -> None:
|
||||
channel = Topic(str, accumulate=True).from_checkpoint(None)
|
||||
channel = Topic(str, accumulate=True).from_checkpoint(MISSING)
|
||||
assert channel.ValueType is Sequence[str]
|
||||
assert channel.UpdateType is Union[str, list[str]]
|
||||
|
||||
@@ -73,7 +74,7 @@ def test_topic_accumulate() -> None:
|
||||
|
||||
|
||||
def test_binop() -> None:
|
||||
channel = BinaryOperatorAggregate(int, operator.add).from_checkpoint(None)
|
||||
channel = BinaryOperatorAggregate(int, operator.add).from_checkpoint(MISSING)
|
||||
assert channel.ValueType is int
|
||||
assert channel.UpdateType is int
|
||||
|
||||
|
||||
@@ -1310,8 +1310,8 @@ def test_pending_writes_resume(
|
||||
},
|
||||
"channel_values": {
|
||||
"value": 1,
|
||||
"branch:to:one": "__start__",
|
||||
"branch:to:two": "__start__",
|
||||
"branch:to:one": None,
|
||||
"branch:to:two": None,
|
||||
},
|
||||
},
|
||||
metadata={
|
||||
@@ -1363,8 +1363,8 @@ def test_pending_writes_resume(
|
||||
parent_config=None,
|
||||
pending_writes=UnsortedSequence(
|
||||
(AnyStr(), "value", 1),
|
||||
(AnyStr(), "branch:to:one", "__start__"),
|
||||
(AnyStr(), "branch:to:two", "__start__"),
|
||||
(AnyStr(), "branch:to:one", None),
|
||||
(AnyStr(), "branch:to:two", None),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -2146,8 +2146,8 @@ async def test_pending_writes_resume(
|
||||
},
|
||||
"channel_values": {
|
||||
"value": 1,
|
||||
"branch:to:one": "__start__",
|
||||
"branch:to:two": "__start__",
|
||||
"branch:to:one": None,
|
||||
"branch:to:two": None,
|
||||
},
|
||||
},
|
||||
metadata={
|
||||
@@ -2201,8 +2201,8 @@ async def test_pending_writes_resume(
|
||||
parent_config=None,
|
||||
pending_writes=UnsortedSequence(
|
||||
(AnyStr(), "value", 1),
|
||||
(AnyStr(), "branch:to:one", "__start__"),
|
||||
(AnyStr(), "branch:to:two", "__start__"),
|
||||
(AnyStr(), "branch:to:one", None),
|
||||
(AnyStr(), "branch:to:two", None),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user