Skip to content

Commit a6423b0

Browse files
authored
Clean up some key and UUID->str conversion handling (#15)
* Clean up some key and UUID->str conversion handling * Silence mypy error
1 parent d9c566a commit a6423b0

File tree

4 files changed

+45
-62
lines changed

4 files changed

+45
-62
lines changed

langgraph/checkpoint/redis/__init__.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,10 @@ def put(
214214

215215
thread_id = configurable.pop("thread_id")
216216
checkpoint_ns = configurable.pop("checkpoint_ns")
217-
checkpoint_id = checkpoint_id = configurable.pop(
218-
"checkpoint_id", configurable.pop("thread_ts", "")
217+
thread_ts = configurable.pop("thread_ts", "")
218+
checkpoint_id = (
219+
configurable.pop("checkpoint_id", configurable.pop("thread_ts", ""))
220+
or thread_ts
219221
)
220222

221223
# For values we store in Redis, we need to convert empty strings to the

langgraph/checkpoint/redis/aio.py

+21-10
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from collections.abc import AsyncIterator
88
from contextlib import asynccontextmanager
99
from functools import partial
10-
from sys import thread_info
1110
from types import TracebackType
1211
from typing import Any, List, Optional, Sequence, Tuple, Type, cast
1312

@@ -375,10 +374,20 @@ async def aput(
375374
) -> RunnableConfig:
376375
"""Store a checkpoint to Redis."""
377376
configurable = config["configurable"].copy()
377+
378378
thread_id = configurable.pop("thread_id")
379379
checkpoint_ns = configurable.pop("checkpoint_ns")
380380
thread_ts = configurable.pop("thread_ts", "")
381-
checkpoint_id = configurable.pop("checkpoint_id", thread_ts) or thread_ts
381+
checkpoint_id = (
382+
configurable.pop("checkpoint_id", configurable.pop("thread_ts", ""))
383+
or thread_ts
384+
)
385+
386+
# For values we store in Redis, we need to convert empty strings to the
387+
# sentinel value.
388+
storage_safe_thread_id = to_storage_safe_id(thread_id)
389+
storage_safe_checkpoint_ns = to_storage_safe_str(checkpoint_ns)
390+
storage_safe_checkpoint_id = to_storage_safe_id(checkpoint_id)
382391

383392
copy = checkpoint.copy()
384393
next_config = {
@@ -391,32 +400,34 @@ async def aput(
391400

392401
# Store checkpoint data
393402
checkpoint_data = {
394-
"thread_id": thread_id,
395-
"checkpoint_ns": to_storage_safe_str(checkpoint_ns),
396-
"checkpoint_id": to_storage_safe_id(checkpoint_id),
397-
"parent_checkpoint_id": to_storage_safe_id(checkpoint_id),
403+
"thread_id": storage_safe_thread_id,
404+
"checkpoint_ns": storage_safe_checkpoint_ns,
405+
"checkpoint_id": storage_safe_checkpoint_id,
406+
"parent_checkpoint_id": storage_safe_checkpoint_id,
398407
"checkpoint": self._dump_checkpoint(copy),
399408
"metadata": self._dump_metadata(metadata),
400409
}
401410

402411
# store at top-level for filters in list()
403412
if all(key in metadata for key in ["source", "step"]):
404413
checkpoint_data["source"] = metadata["source"]
405-
checkpoint_data["step"] = metadata["step"]
414+
checkpoint_data["step"] = metadata["step"] # type: ignore
406415

407416
await self.checkpoints_index.load(
408417
[checkpoint_data],
409418
keys=[
410419
BaseRedisSaver._make_redis_checkpoint_key(
411-
thread_id, checkpoint_ns, checkpoint_id
420+
storage_safe_thread_id,
421+
storage_safe_checkpoint_ns,
422+
storage_safe_checkpoint_id,
412423
)
413424
],
414425
)
415426

416427
# Store blob values
417428
blobs = self._dump_blobs(
418-
thread_id,
419-
checkpoint_ns,
429+
storage_safe_thread_id,
430+
storage_safe_checkpoint_ns,
420431
copy.get("channel_values", {}),
421432
new_versions,
422433
)

langgraph/checkpoint/redis/base.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -457,9 +457,9 @@ def _make_redis_checkpoint_key(
457457
return REDIS_KEY_SEPARATOR.join(
458458
[
459459
CHECKPOINT_PREFIX,
460-
to_storage_safe_id(thread_id),
460+
str(to_storage_safe_id(thread_id)),
461461
to_storage_safe_str(checkpoint_ns),
462-
to_storage_safe_id(checkpoint_id),
462+
str(to_storage_safe_id(checkpoint_id)),
463463
]
464464
)
465465

@@ -470,7 +470,7 @@ def _make_redis_checkpoint_blob_key(
470470
return REDIS_KEY_SEPARATOR.join(
471471
[
472472
CHECKPOINT_BLOB_PREFIX,
473-
to_storage_safe_str(thread_id),
473+
str(to_storage_safe_id(thread_id)),
474474
to_storage_safe_str(checkpoint_ns),
475475
channel,
476476
version,
@@ -485,9 +485,9 @@ def _make_redis_checkpoint_writes_key(
485485
task_id: str,
486486
idx: Optional[int],
487487
) -> str:
488-
storage_safe_thread_id = to_storage_safe_str(thread_id)
488+
storage_safe_thread_id = str(to_storage_safe_id(thread_id))
489489
storage_safe_checkpoint_ns = to_storage_safe_str(checkpoint_ns)
490-
storage_safe_checkpoint_id = to_storage_safe_str(checkpoint_id)
490+
storage_safe_checkpoint_id = str(to_storage_safe_id(checkpoint_id))
491491

492492
if idx is None:
493493
return REDIS_KEY_SEPARATOR.join(

langgraph/checkpoint/redis/util.py

+15-45
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from typing import Any, Callable, Optional, TypeVar, Union
2-
31
"""
42
RediSearch versions below 2.10 don't support indexing and querying
53
empty strings, so we use a sentinel value to represent empty strings.
@@ -8,14 +6,17 @@
86
sentinel values are from the first run of the graph, so this should
97
generally be correct.
108
"""
9+
1110
EMPTY_STRING_SENTINEL = "__empty__"
1211
EMPTY_ID_SENTINEL = "00000000-0000-0000-0000-000000000000"
1312

1413

1514
def to_storage_safe_str(value: str) -> str:
1615
"""
17-
Convert any empty string to an empty string sentinel if found,
18-
otherwise return the value unchanged.
16+
Prepare a value for storage in Redis as a string.
17+
18+
Convert an empty string to a sentinel value, otherwise return the
19+
value as a string.
1920
2021
Args:
2122
value (str): The value to convert.
@@ -26,13 +27,13 @@ def to_storage_safe_str(value: str) -> str:
2627
if value == "":
2728
return EMPTY_STRING_SENTINEL
2829
else:
29-
return value
30+
return str(value)
3031

3132

3233
def from_storage_safe_str(value: str) -> str:
3334
"""
34-
Convert a value from an empty string sentinel to an empty string
35-
if found, otherwise return the value unchanged.
35+
Convert a value from a sentinel value to an empty string if present,
36+
otherwise return the value unchanged.
3637
3738
Args:
3839
value (str): The value to convert.
@@ -48,8 +49,10 @@ def from_storage_safe_str(value: str) -> str:
4849

4950
def to_storage_safe_id(value: str) -> str:
5051
"""
51-
Convert any empty ID string to an empty ID sentinel if found,
52-
otherwise return the value unchanged.
52+
Prepare a value for storage in Redis as an ID.
53+
54+
Convert an empty string to a sentinel value for empty ID strings, otherwise
55+
return the value as a string.
5356
5457
Args:
5558
value (str): The value to convert.
@@ -60,13 +63,13 @@ def to_storage_safe_id(value: str) -> str:
6063
if value == "":
6164
return EMPTY_ID_SENTINEL
6265
else:
63-
return value
66+
return str(value)
6467

6568

6669
def from_storage_safe_id(value: str) -> str:
6770
"""
68-
Convert a value from an empty ID sentinel to an empty ID
69-
if found, otherwise return the value unchanged.
71+
Convert a value from a sentinel value for empty ID strings to an empty
72+
ID string if present, otherwise return the value unchanged.
7073
7174
Args:
7275
value (str): The value to convert.
@@ -78,36 +81,3 @@ def from_storage_safe_id(value: str) -> str:
7881
return ""
7982
else:
8083
return value
81-
82-
83-
def storage_safe_get(
84-
doc: dict[str, Any], key: str, default: Any = None
85-
) -> Optional[Any]:
86-
"""
87-
Get a value from a Redis document or dictionary, using a sentinel
88-
value to represent empty strings.
89-
90-
If the sentinel value is found, it is converted back to an empty string.
91-
92-
Args:
93-
doc (dict[str, Any]): The document to get the value from.
94-
key (str): The key to get the value from.
95-
default (Any): The default value to return if the key is not found.
96-
Returns:
97-
Optional[Any]: None if the key is not found, or else the value from
98-
the document or dictionary, with empty strings converted
99-
to the empty string sentinel and the sentinel converted
100-
back to an empty string.
101-
"""
102-
try:
103-
# NOTE: The Document class that comes back from `search()` support
104-
# [key] access but not `get()` for some reason, so we use direct
105-
# key access with an exception guard.
106-
value = doc[key]
107-
except KeyError:
108-
value = None
109-
110-
if value is None:
111-
return default
112-
113-
return to_storage_safe_str(value)

0 commit comments

Comments
 (0)