From 51f6b2417f7c567ff5a6e70b9d801b8f6fd767bf Mon Sep 17 00:00:00 2001 From: Brian Sam-Bodden Date: Fri, 19 Dec 2025 17:56:05 -0700 Subject: [PATCH 1/2] chore(release): bump version to 0.3.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 15e3a9a..48110da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-checkpoint-redis" -version = "0.3.0" +version = "0.3.1" description = "Redis implementation of the LangGraph agent checkpoint saver and store." authors = ["Redis Inc. ", "Brian Sam-Bodden "] license = "MIT" From b585fa784c18370210647cf319b1eb2a0a6d050a Mon Sep 17 00:00:00 2001 From: Brian Sam-Bodden Date: Fri, 19 Dec 2025 19:26:36 -0700 Subject: [PATCH 2/2] fix(types): resolve mypy errors from redis-py incorrect type annotations The redis-py library has incorrect type annotations for JSON commands: - `json().get()` is typed as returning `List[JsonType]` but returns `JsonType` - Async `json().set()`/`json().get()` are typed as non-awaitable These typing issues manifest differently across Python versions (3.10-3.13), causing CI failures while passing locally. Changes: - Disable `warn_unused_ignores` in mypy config for cross-version compatibility - Add `cast()` for `json().get()` results where dict access is needed - Add `# type: ignore[misc]` for async JSON operations --- langgraph/checkpoint/redis/aio.py | 10 +++++----- langgraph/checkpoint/redis/ashallow.py | 4 ++-- langgraph/checkpoint/redis/base.py | 3 ++- langgraph/store/redis/__init__.py | 3 ++- langgraph/store/redis/aio.py | 2 +- pyproject.toml | 2 +- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/langgraph/checkpoint/redis/aio.py b/langgraph/checkpoint/redis/aio.py index 6962694..8a4e4c4 100644 --- a/langgraph/checkpoint/redis/aio.py +++ b/langgraph/checkpoint/redis/aio.py @@ -1063,7 +1063,7 @@ async def aput( if self.cluster_mode: # For cluster mode, execute operation directly - await self._redis.json().set( + await self._redis.json().set( # type: ignore[misc] checkpoint_key, "$", checkpoint_data ) else: @@ -1146,7 +1146,7 @@ async def aput_writes( ) # Redis JSON.SET is an UPSERT by default - await self._redis.json().set(key, "$", cast(Any, write_obj)) + await self._redis.json().set(key, "$", cast(Any, write_obj)) # type: ignore[misc] created_keys.append(key) # Apply TTL to newly created keys @@ -1304,14 +1304,14 @@ async def aput_writes( # Update has_writes flag separately for older Redis if checkpoint_key: try: - checkpoint_data = await self._redis.json().get( + checkpoint_data = await self._redis.json().get( # type: ignore[misc] checkpoint_key ) if isinstance( checkpoint_data, dict ) and not checkpoint_data.get("has_writes"): checkpoint_data["has_writes"] = True - await self._redis.json().set( + await self._redis.json().set( # type: ignore[misc] checkpoint_key, "$", checkpoint_data ) except Exception: @@ -1479,7 +1479,7 @@ async def aget_channel_values( ) # Single JSON.GET operation to retrieve checkpoint with inline channel_values - checkpoint_data = await self._redis.json().get(checkpoint_key, "$.checkpoint") + checkpoint_data = await self._redis.json().get(checkpoint_key, "$.checkpoint") # type: ignore[misc] if not checkpoint_data: return {} diff --git a/langgraph/checkpoint/redis/ashallow.py b/langgraph/checkpoint/redis/ashallow.py index db8113c..a0efe4a 100644 --- a/langgraph/checkpoint/redis/ashallow.py +++ b/langgraph/checkpoint/redis/ashallow.py @@ -365,7 +365,7 @@ async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: ) # Single fetch gets everything inline - matching sync implementation - full_checkpoint_data = await self._redis.json().get(checkpoint_key) + full_checkpoint_data = await self._redis.json().get(checkpoint_key) # type: ignore[misc] if not full_checkpoint_data or not isinstance(full_checkpoint_data, dict): return None @@ -544,7 +544,7 @@ async def aget_channel_values( ) # Single JSON.GET operation to retrieve checkpoint with inline channel_values - checkpoint_data = await self._redis.json().get(checkpoint_key, "$.checkpoint") + checkpoint_data = await self._redis.json().get(checkpoint_key, "$.checkpoint") # type: ignore[misc] if not checkpoint_data: return {} diff --git a/langgraph/checkpoint/redis/base.py b/langgraph/checkpoint/redis/base.py index 049c81a..56a2091 100644 --- a/langgraph/checkpoint/redis/base.py +++ b/langgraph/checkpoint/redis/base.py @@ -558,7 +558,8 @@ def _load_writes_from_redis(self, write_key: str) -> List[Tuple[str, str, Any]]: return [] # Get the full JSON document - result = self._redis.json().get(write_key) + # Cast needed: redis-py types json().get() as List[JsonType] but returns dict + result = cast(Optional[Dict[str, Any]], self._redis.json().get(write_key)) if not result: return [] diff --git a/langgraph/store/redis/__init__.py b/langgraph/store/redis/__init__.py index e3d4629..a073f44 100644 --- a/langgraph/store/redis/__init__.py +++ b/langgraph/store/redis/__init__.py @@ -540,8 +540,9 @@ def _batch_search_ops( score = (1.0 - float(dist)) if dist is not None else 0.0 if not isinstance(store_doc, dict): try: + # Cast needed: redis-py types json().get() incorrectly store_doc = json.loads( - store_doc + cast(str, store_doc) ) # Attempt to parse if it's a JSON string except (json.JSONDecodeError, TypeError): logger.error(f"Failed to parse store_doc: {store_doc}") diff --git a/langgraph/store/redis/aio.py b/langgraph/store/redis/aio.py index 3e2dec4..4458eed 100644 --- a/langgraph/store/redis/aio.py +++ b/langgraph/store/redis/aio.py @@ -781,7 +781,7 @@ async def _batch_search_ops( ) result_map[store_key] = doc # Fetch individually in cluster mode - store_doc_item = await self._redis.json().get(store_key) + store_doc_item = await self._redis.json().get(store_key) # type: ignore[misc] store_docs.append(store_doc_item) store_docs_raw = store_docs else: diff --git a/pyproject.toml b/pyproject.toml index 48110da..20e2576 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,7 @@ exclude = ''' disallow_untyped_defs = true explicit_package_bases = true warn_no_return = false -warn_unused_ignores = true +warn_unused_ignores = false warn_redundant_casts = true allow_redefinition = true ignore_missing_imports = true