mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-06 01:37:49 +02:00
Add HTTP deserialization option (#4002)
This commit is contained in:
@@ -33,6 +33,15 @@ LC_REVIVER = Reviver()
|
||||
|
||||
|
||||
class JsonPlusSerializer(SerializerProtocol):
|
||||
def __init__(
|
||||
self, *, __unpack_ext_hook__: Optional[Callable[[int, bytes], Any]] = None
|
||||
) -> None:
|
||||
self._unpack_ext_hook = (
|
||||
__unpack_ext_hook__
|
||||
if __unpack_ext_hook__ is not None
|
||||
else _msgpack_ext_hook
|
||||
)
|
||||
|
||||
def _encode_constructor_args(
|
||||
self,
|
||||
constructor: Union[Callable, type[Any]],
|
||||
@@ -210,7 +219,7 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
return self.loads(data_)
|
||||
elif type_ == "msgpack":
|
||||
return ormsgpack.unpackb(
|
||||
data_, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
data_, ext_hook=self._unpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown serialization type: {type_}")
|
||||
@@ -521,6 +530,82 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
|
||||
return
|
||||
|
||||
|
||||
def _msgpack_ext_hook_to_json(code: int, data: bytes) -> Any:
|
||||
if code == EXT_CONSTRUCTOR_SINGLE_ARG:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data,
|
||||
ext_hook=_msgpack_ext_hook_to_json,
|
||||
option=ormsgpack.OPT_NON_STR_KEYS,
|
||||
)
|
||||
if tup[0] == "uuid" and tup[1] == "UUID":
|
||||
hex_ = tup[2]
|
||||
return (
|
||||
f"{hex_[:8]}-{hex_[8:12]}-{hex_[12:16]}-{hex_[16:20]}-{hex_[20:]}"
|
||||
)
|
||||
# module, name, arg
|
||||
return tup[2]
|
||||
except Exception:
|
||||
return
|
||||
elif code == EXT_CONSTRUCTOR_POS_ARGS:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data,
|
||||
ext_hook=_msgpack_ext_hook_to_json,
|
||||
option=ormsgpack.OPT_NON_STR_KEYS,
|
||||
)
|
||||
# module, name, args
|
||||
return tup[2]
|
||||
except Exception:
|
||||
return
|
||||
elif code == EXT_CONSTRUCTOR_KW_ARGS:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data,
|
||||
ext_hook=_msgpack_ext_hook_to_json,
|
||||
option=ormsgpack.OPT_NON_STR_KEYS,
|
||||
)
|
||||
# module, name, args
|
||||
return tup[2]
|
||||
except Exception:
|
||||
return
|
||||
elif code == EXT_METHOD_SINGLE_ARG:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data,
|
||||
ext_hook=_msgpack_ext_hook_to_json,
|
||||
option=ormsgpack.OPT_NON_STR_KEYS,
|
||||
)
|
||||
# module, name, arg, method
|
||||
return tup[2]
|
||||
except Exception:
|
||||
return
|
||||
elif code == EXT_PYDANTIC_V1:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data,
|
||||
ext_hook=_msgpack_ext_hook_to_json,
|
||||
option=ormsgpack.OPT_NON_STR_KEYS,
|
||||
)
|
||||
# module, name, kwargs
|
||||
return tup[2]
|
||||
except Exception:
|
||||
# for pydantic objects we can't find/reconstruct
|
||||
# let's return the kwargs dict instead
|
||||
return
|
||||
elif code == EXT_PYDANTIC_V2:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data,
|
||||
ext_hook=_msgpack_ext_hook_to_json,
|
||||
option=ormsgpack.OPT_NON_STR_KEYS,
|
||||
)
|
||||
# module, name, kwargs, method
|
||||
return tup[2]
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
_option = (
|
||||
ormsgpack.OPT_NON_STR_KEYS
|
||||
| ormsgpack.OPT_PASSTHROUGH_DATACLASS
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.21"
|
||||
version = "2.0.22"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -15,7 +15,10 @@ from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from pydantic.v1 import SecretStr as SecretStrV1
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
from langgraph.checkpoint.serde.jsonplus import (
|
||||
JsonPlusSerializer,
|
||||
_msgpack_ext_hook_to_json,
|
||||
)
|
||||
from langgraph.store.base import Item
|
||||
|
||||
|
||||
@@ -165,6 +168,113 @@ def test_serde_jsonplus() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_serde_jsonplus_json_mode() -> None:
|
||||
uid = uuid.UUID(int=1)
|
||||
deque_instance = deque([1, 2, 3])
|
||||
tzn = ZoneInfo("America/New_York")
|
||||
ip4 = IPv4Address("192.168.0.1")
|
||||
current_date = date(2024, 4, 19)
|
||||
current_time = time(23, 4, 57, 51022, timezone.max)
|
||||
current_timestamp = datetime(2024, 4, 19, 23, 4, 57, 51022, timezone.max)
|
||||
|
||||
to_serialize = {
|
||||
"path": pathlib.Path("foo", "bar"),
|
||||
"re": re.compile(r"foo", re.DOTALL),
|
||||
"decimal": Decimal("1.10101"),
|
||||
"set": {1, 2, frozenset({1, 2})},
|
||||
"frozen_set": frozenset({1, 2, 3}),
|
||||
"ip4": ip4,
|
||||
"deque": deque_instance,
|
||||
"tzn": tzn,
|
||||
"date": current_date,
|
||||
"time": current_time,
|
||||
"uid": uid,
|
||||
"timestamp": current_timestamp,
|
||||
"my_slotted_class": MyDataclassWSlots("bar", 2, InnerDataclass("hello")),
|
||||
"my_dataclass": MyDataclass("foo", 1, InnerDataclass("hello")),
|
||||
"my_enum": MyEnum.FOO,
|
||||
"my_pydantic": MyPydantic(foo="foo", bar=1, inner=InnerPydantic(hello="hello")),
|
||||
"my_pydantic_v1": MyPydanticV1(
|
||||
foo="foo", bar=1, inner=InnerPydanticV1(hello="hello")
|
||||
),
|
||||
"my_secret_str": SecretStr("meow"),
|
||||
"my_secret_str_v1": SecretStrV1("meow"),
|
||||
"person": Person(name="foo"),
|
||||
"a_bool": True,
|
||||
"a_none": None,
|
||||
"a_str": "foo",
|
||||
"a_str_nuc": "foo\u0000",
|
||||
"a_str_uc": "foo ⛰️",
|
||||
"a_str_ucuc": "foo \u26f0\ufe0f\u0000",
|
||||
"a_str_ucucuc": "foo \\u26f0\\ufe0f",
|
||||
"an_int": 1,
|
||||
"a_float": 1.1,
|
||||
"a_bytes": b"my bytes",
|
||||
"a_bytearray": bytearray([42]),
|
||||
"my_item": Item(
|
||||
value={},
|
||||
key="my-key",
|
||||
namespace=("a", "name", " "),
|
||||
created_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
|
||||
updated_at=datetime(2024, 9, 24, 17, 29, 11, 128397),
|
||||
),
|
||||
}
|
||||
|
||||
serde = JsonPlusSerializer(__unpack_ext_hook__=_msgpack_ext_hook_to_json)
|
||||
|
||||
dumped = serde.dumps_typed(to_serialize)
|
||||
|
||||
assert dumped[0] == "msgpack"
|
||||
result = serde.loads_typed(dumped)
|
||||
assert result == {
|
||||
"path": ["foo", "bar"],
|
||||
"re": ["foo", 48],
|
||||
"decimal": "1.10101",
|
||||
"set": [1, 2, [1, 2]],
|
||||
"frozen_set": [1, 2, 3],
|
||||
"ip4": "192.168.0.1",
|
||||
"deque": [1, 2, 3],
|
||||
"tzn": "America/New_York",
|
||||
"date": [2024, 4, 19],
|
||||
"time": {
|
||||
"hour": 23,
|
||||
"minute": 4,
|
||||
"second": 57,
|
||||
"microsecond": 51022,
|
||||
"tzinfo": [[0, 86340, 0]],
|
||||
"fold": 0,
|
||||
},
|
||||
"uid": "00000000-0000-0000-0000-000000000001",
|
||||
"timestamp": "2024-04-19T23:04:57.051022+23:59",
|
||||
"my_slotted_class": {"foo": "bar", "bar": 2, "inner": {"hello": "hello"}},
|
||||
"my_dataclass": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
|
||||
"my_enum": "foo",
|
||||
"my_pydantic": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
|
||||
"my_pydantic_v1": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
|
||||
"my_secret_str": "meow",
|
||||
"my_secret_str_v1": "meow",
|
||||
"person": {"name": "foo"},
|
||||
"a_bool": True,
|
||||
"a_none": None,
|
||||
"a_str": "foo",
|
||||
"a_str_nuc": "foo\x00",
|
||||
"a_str_uc": "foo ⛰️",
|
||||
"a_str_ucuc": "foo ⛰️\x00",
|
||||
"a_str_ucucuc": "foo \\u26f0\\ufe0f",
|
||||
"an_int": 1,
|
||||
"a_float": 1.1,
|
||||
"a_bytes": b"my bytes",
|
||||
"a_bytearray": b"*",
|
||||
"my_item": {
|
||||
"namespace": ["a", "name", " "],
|
||||
"key": "my-key",
|
||||
"value": {},
|
||||
"created_at": "2024-09-24T17:29:10.128397",
|
||||
"updated_at": "2024-09-24T17:29:11.128397",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_serde_jsonplus_bytes() -> None:
|
||||
serde = JsonPlusSerializer()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user