Skip to content

Commit 49ae3aa

Browse files
committed
test(store): add tests for LangChain message serialization (#128)
Add comprehensive test suite for issue #128 covering: - Single HumanMessage storage and retrieval - Multiple message types (SystemMessage, HumanMessage, AIMessage) - Messages with additional kwargs (name, id, additional_kwargs) - Nested message structures in complex data - AIMessage with tool_calls - Search operations with message values
1 parent 719f0fd commit 49ae3aa

File tree

1 file changed

+211
-0
lines changed

1 file changed

+211
-0
lines changed
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
"""Tests for issue #128: HumanMessage serialization in RedisStore.
2+
3+
This test reproduces the issue where storing LangChain message objects in
4+
RedisStore fails with:
5+
redisvl.exceptions.RedisVLerror: failed to load data: Object of type
6+
HumanMessage is not JSON serializable
7+
8+
The root cause is that RedisStore passes values directly to redisvl's
9+
SearchIndex.load() which uses standard JSON serialization, unable to
10+
handle LangChain message objects.
11+
"""
12+
13+
from __future__ import annotations
14+
15+
from typing import Iterator
16+
17+
import pytest
18+
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
19+
20+
from langgraph.store.redis import RedisStore
21+
22+
23+
@pytest.fixture(scope="function")
24+
def store(redis_url: str) -> Iterator[RedisStore]:
25+
"""Fixture to create a Redis store."""
26+
with RedisStore.from_conn_string(redis_url) as store:
27+
store.setup()
28+
yield store
29+
30+
31+
class TestIssue128MessageSerialization:
32+
"""Test suite for issue #128: HumanMessage serialization in RedisStore."""
33+
34+
def test_store_human_message_in_value(self, store: RedisStore) -> None:
35+
"""Test storing a HumanMessage object directly in the store value.
36+
37+
This is the core issue: storing LangChain message objects fails because
38+
redisvl cannot serialize them with standard JSON.
39+
"""
40+
namespace = ("test", "messages")
41+
key = "message1"
42+
43+
# This is the value that causes the issue - contains HumanMessage
44+
value = {
45+
"messages": [
46+
HumanMessage(content="Hello, how are you?"),
47+
]
48+
}
49+
50+
# This should work but currently fails with:
51+
# redisvl.exceptions.RedisVLerror: failed to load data:
52+
# Object of type HumanMessage is not JSON serializable
53+
store.put(namespace, key, value)
54+
55+
# Verify we can retrieve it back
56+
item = store.get(namespace, key)
57+
assert item is not None
58+
assert "messages" in item.value
59+
assert len(item.value["messages"]) == 1
60+
61+
# Verify the message is properly deserialized
62+
retrieved_message = item.value["messages"][0]
63+
assert isinstance(retrieved_message, HumanMessage)
64+
assert retrieved_message.content == "Hello, how are you?"
65+
66+
def test_store_multiple_message_types(self, store: RedisStore) -> None:
67+
"""Test storing multiple message types in the store value."""
68+
namespace = ("test", "conversation")
69+
key = "conv1"
70+
71+
value = {
72+
"messages": [
73+
SystemMessage(content="You are a helpful assistant."),
74+
HumanMessage(content="What is Python?"),
75+
AIMessage(content="Python is a programming language."),
76+
]
77+
}
78+
79+
store.put(namespace, key, value)
80+
81+
item = store.get(namespace, key)
82+
assert item is not None
83+
assert len(item.value["messages"]) == 3
84+
85+
# Verify message types are preserved
86+
messages = item.value["messages"]
87+
assert isinstance(messages[0], SystemMessage)
88+
assert isinstance(messages[1], HumanMessage)
89+
assert isinstance(messages[2], AIMessage)
90+
91+
def test_store_message_with_additional_kwargs(self, store: RedisStore) -> None:
92+
"""Test storing messages with additional kwargs like name, id, etc."""
93+
namespace = ("test", "messages")
94+
key = "msg_with_kwargs"
95+
96+
value = {
97+
"messages": [
98+
HumanMessage(
99+
content="Hello!",
100+
name="User",
101+
id="msg-123",
102+
additional_kwargs={"custom": "data"},
103+
),
104+
]
105+
}
106+
107+
store.put(namespace, key, value)
108+
109+
item = store.get(namespace, key)
110+
assert item is not None
111+
112+
retrieved_message = item.value["messages"][0]
113+
assert isinstance(retrieved_message, HumanMessage)
114+
assert retrieved_message.content == "Hello!"
115+
assert retrieved_message.name == "User"
116+
assert retrieved_message.id == "msg-123"
117+
assert retrieved_message.additional_kwargs == {"custom": "data"}
118+
119+
def test_store_nested_message_structure(self, store: RedisStore) -> None:
120+
"""Test storing messages in nested data structures."""
121+
namespace = ("test", "nested")
122+
key = "nested1"
123+
124+
value = {
125+
"conversation": {
126+
"id": "conv-1",
127+
"messages": [
128+
HumanMessage(content="First message"),
129+
],
130+
"metadata": {
131+
"last_message": HumanMessage(content="Last message"),
132+
},
133+
}
134+
}
135+
136+
store.put(namespace, key, value)
137+
138+
item = store.get(namespace, key)
139+
assert item is not None
140+
141+
# Verify nested messages are preserved
142+
conv = item.value["conversation"]
143+
assert isinstance(conv["messages"][0], HumanMessage)
144+
assert isinstance(conv["metadata"]["last_message"], HumanMessage)
145+
146+
def test_store_ai_message_with_tool_calls(self, store: RedisStore) -> None:
147+
"""Test storing AIMessage with tool calls."""
148+
namespace = ("test", "tools")
149+
key = "tool_call"
150+
151+
value = {
152+
"messages": [
153+
AIMessage(
154+
content="",
155+
tool_calls=[
156+
{
157+
"id": "call_123",
158+
"name": "get_weather",
159+
"args": {"location": "NYC"},
160+
}
161+
],
162+
),
163+
]
164+
}
165+
166+
store.put(namespace, key, value)
167+
168+
item = store.get(namespace, key)
169+
assert item is not None
170+
171+
retrieved_message = item.value["messages"][0]
172+
assert isinstance(retrieved_message, AIMessage)
173+
assert len(retrieved_message.tool_calls) == 1
174+
assert retrieved_message.tool_calls[0]["name"] == "get_weather"
175+
176+
def test_search_with_message_values(self, store: RedisStore) -> None:
177+
"""Test searching for items that contain message values.
178+
179+
Note: When values contain non-JSON-serializable objects like HumanMessage,
180+
the entire value is serialized using the serde wrapper. This means filters
181+
on nested fields won't work for such values. This test verifies that
182+
search works and messages are properly deserialized.
183+
"""
184+
namespace = ("test", "searchable")
185+
186+
# Store items with messages
187+
for i in range(3):
188+
store.put(
189+
namespace,
190+
f"msg{i}",
191+
{
192+
"topic": f"topic_{i}",
193+
"messages": [HumanMessage(content=f"Message {i}")],
194+
},
195+
)
196+
197+
# Search without filter (filters don't work on serialized values)
198+
results = store.search(namespace)
199+
assert len(results) == 3
200+
201+
# Verify messages are properly deserialized in all results
202+
for result in results:
203+
assert "messages" in result.value
204+
assert isinstance(result.value["messages"][0], HumanMessage)
205+
206+
# Verify we can get specific items by key
207+
item = store.get(namespace, "msg1")
208+
assert item is not None
209+
assert item.value["topic"] == "topic_1"
210+
assert isinstance(item.value["messages"][0], HumanMessage)
211+
assert item.value["messages"][0].content == "Message 1"

0 commit comments

Comments
 (0)