99from datetime import datetime , timedelta , timezone
1010from typing import (
1111 Any ,
12+ Callable ,
1213 Dict ,
1314 Generic ,
1415 Iterable ,
4041from redisvl .query .filter import Tag , Text
4142from redisvl .utils .token_escaper import TokenEscaper
4243
44+ from langgraph .checkpoint .redis .jsonplus_redis import JsonPlusRedisSerializer
45+
4346from .token_unescaper import TokenUnescaper
4447from .types import IndexType , RedisClientType
4548
@@ -124,6 +127,9 @@ class BaseRedisStore(Generic[RedisClientType, IndexType]):
124127 supports_ttl : bool = True
125128 ttl_config : Optional [TTLConfig ] = None
126129
130+ # Serializer for handling complex objects like LangChain messages
131+ _serde : JsonPlusRedisSerializer
132+
127133 def _apply_ttl_to_keys (
128134 self ,
129135 main_key : str ,
@@ -223,6 +229,8 @@ def __init__(
223229 self ._redis = conn
224230 # Store cluster_mode; None means auto-detect in RedisStore or AsyncRedisStore
225231 self .cluster_mode = cluster_mode
232+ # Initialize the serializer for handling complex objects like LangChain messages
233+ self ._serde = JsonPlusRedisSerializer ()
226234
227235 # Store custom prefixes
228236 self .store_prefix = store_prefix
@@ -357,6 +365,84 @@ async def aset_client_info(self) -> None:
357365 # Silently fail if even echo doesn't work
358366 pass
359367
368+ def _serialize_value (self , value : Any ) -> Any :
369+ """Serialize a value for storage in Redis.
370+
371+ This method handles complex objects like LangChain messages by
372+ serializing them to a JSON-compatible format.
373+
374+ The method is smart about serialization:
375+ - If the value is a simple JSON-serializable dict/list, it's stored as-is
376+ - If the value contains complex objects (HumanMessage, etc.), it uses
377+ the serde wrapper format
378+
379+ Args:
380+ value: The value to serialize (can contain HumanMessage, AIMessage, etc.)
381+
382+ Returns:
383+ A JSON-serializable representation of the value
384+ """
385+ import json
386+
387+ if value is None :
388+ return None
389+
390+ # First, try standard JSON serialization to check if it's needed
391+ try :
392+ json .dumps (value )
393+ # Value is already JSON-serializable, return as-is for backward
394+ # compatibility and to preserve filter functionality
395+ return value
396+ except (TypeError , ValueError ):
397+ # Value contains non-JSON-serializable objects, use serde wrapper
398+ pass
399+
400+ # Use the serializer to handle complex objects
401+ type_str , data_bytes = self ._serde .dumps_typed (value )
402+ # Store the serialized data with type info for proper deserialization
403+ return {
404+ "__serde_type__" : type_str ,
405+ "__serde_data__" : (
406+ data_bytes .decode ("utf-8" ) if type_str == "json" else data_bytes .hex ()
407+ ),
408+ }
409+
410+ def _deserialize_value (self , value : Any ) -> Any :
411+ """Deserialize a value from Redis storage.
412+
413+ This method handles both new serialized format and legacy plain values
414+ for backward compatibility.
415+
416+ Args:
417+ value: The value from Redis (may be serialized or plain)
418+
419+ Returns:
420+ The deserialized value with proper Python objects (HumanMessage, etc.)
421+ """
422+ if value is None :
423+ return None
424+
425+ # Check if this is a serialized value (new format)
426+ if (
427+ isinstance (value , dict )
428+ and "__serde_type__" in value
429+ and "__serde_data__" in value
430+ ):
431+ type_str = value ["__serde_type__" ]
432+ data_str = value ["__serde_data__" ]
433+
434+ # Convert back to bytes
435+ if type_str == "json" :
436+ data_bytes = data_str .encode ("utf-8" )
437+ else :
438+ data_bytes = bytes .fromhex (data_str )
439+
440+ return self ._serde .loads_typed ((type_str , data_bytes ))
441+
442+ # Legacy format: value is stored as-is (plain JSON-serializable data)
443+ # Return as-is for backward compatibility
444+ return value
445+
360446 def _get_batch_GET_ops_queries (
361447 self ,
362448 get_ops : Sequence [tuple [int , GetOp ]],
@@ -433,7 +519,7 @@ def _prepare_batch_PUT_queries(
433519 doc = RedisDocument (
434520 prefix = _namespace_to_text (op .namespace ),
435521 key = op .key ,
436- value = op .value ,
522+ value = self . _serialize_value ( op .value ) ,
437523 created_at = now ,
438524 updated_at = now ,
439525 ttl_minutes = ttl_minutes ,
@@ -568,10 +654,27 @@ def _decode_ns(ns: str) -> tuple[str, ...]:
568654 return tuple (_token_unescaper .unescape (ns ).split ("." ))
569655
570656
571- def _row_to_item (namespace : tuple [str , ...], row : dict [str , Any ]) -> Item :
572- """Convert a row from Redis to an Item."""
657+ def _row_to_item (
658+ namespace : tuple [str , ...],
659+ row : dict [str , Any ],
660+ deserialize_fn : Optional [Callable [[Any ], Any ]] = None ,
661+ ) -> Item :
662+ """Convert a row from Redis to an Item.
663+
664+ Args:
665+ namespace: The namespace tuple for this item
666+ row: The raw row data from Redis
667+ deserialize_fn: Optional function to deserialize the value (handles
668+ LangChain messages, etc.)
669+
670+ Returns:
671+ An Item with properly deserialized value
672+ """
673+ value = row ["value" ]
674+ if deserialize_fn is not None :
675+ value = deserialize_fn (value )
573676 return Item (
574- value = row [ " value" ] ,
677+ value = value ,
575678 key = row ["key" ],
576679 namespace = namespace ,
577680 created_at = datetime .fromtimestamp (row ["created_at" ] / 1_000_000 , timezone .utc ),
@@ -583,10 +686,25 @@ def _row_to_search_item(
583686 namespace : tuple [str , ...],
584687 row : dict [str , Any ],
585688 score : Optional [float ] = None ,
689+ deserialize_fn : Optional [Callable [[Any ], Any ]] = None ,
586690) -> SearchItem :
587- """Convert a row from Redis to a SearchItem."""
691+ """Convert a row from Redis to a SearchItem.
692+
693+ Args:
694+ namespace: The namespace tuple for this item
695+ row: The raw row data from Redis
696+ score: Optional similarity score from vector search
697+ deserialize_fn: Optional function to deserialize the value (handles
698+ LangChain messages, etc.)
699+
700+ Returns:
701+ A SearchItem with properly deserialized value
702+ """
703+ value = row ["value" ]
704+ if deserialize_fn is not None :
705+ value = deserialize_fn (value )
588706 return SearchItem (
589- value = row [ " value" ] ,
707+ value = value ,
590708 key = row ["key" ],
591709 namespace = namespace ,
592710 created_at = datetime .fromtimestamp (row ["created_at" ] / 1_000_000 , timezone .utc ),
0 commit comments