langgraph-checkpoint-sqlite 1.0.2__tar.gz → 1.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {langgraph_checkpoint_sqlite-1.0.2 → langgraph_checkpoint_sqlite-1.0.4}/PKG-INFO +2 -2
- {langgraph_checkpoint_sqlite-1.0.2 → langgraph_checkpoint_sqlite-1.0.4}/langgraph/checkpoint/sqlite/__init__.py +30 -23
- {langgraph_checkpoint_sqlite-1.0.2 → langgraph_checkpoint_sqlite-1.0.4}/langgraph/checkpoint/sqlite/aio.py +54 -10
- {langgraph_checkpoint_sqlite-1.0.2 → langgraph_checkpoint_sqlite-1.0.4}/pyproject.toml +12 -2
- {langgraph_checkpoint_sqlite-1.0.2 → langgraph_checkpoint_sqlite-1.0.4}/README.md +0 -0
- {langgraph_checkpoint_sqlite-1.0.2 → langgraph_checkpoint_sqlite-1.0.4}/langgraph/checkpoint/py.typed +0 -0
- {langgraph_checkpoint_sqlite-1.0.2 → langgraph_checkpoint_sqlite-1.0.4}/langgraph/checkpoint/sqlite/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: langgraph-checkpoint-sqlite
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Library with a SQLite implementation of LangGraph checkpoint saver.
|
|
5
5
|
Home-page: https://www.github.com/langchain-ai/langgraph
|
|
6
6
|
License: MIT
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Requires-Dist: aiosqlite (>=0.20.0,<0.21.0)
|
|
15
|
-
Requires-Dist: langgraph-checkpoint (>=1.0.
|
|
15
|
+
Requires-Dist: langgraph-checkpoint (>=1.0.11,<2.0.0)
|
|
16
16
|
Project-URL: Repository, https://www.github.com/langchain-ai/langgraph
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import random
|
|
1
2
|
import sqlite3
|
|
2
3
|
import threading
|
|
3
4
|
from contextlib import closing, contextmanager
|
|
4
|
-
from hashlib import md5
|
|
5
5
|
from typing import Any, AsyncIterator, Dict, Iterator, Optional, Sequence, Tuple
|
|
6
6
|
|
|
7
7
|
from langchain_core.runnables import RunnableConfig
|
|
@@ -13,7 +13,6 @@ from langgraph.checkpoint.base import (
|
|
|
13
13
|
Checkpoint,
|
|
14
14
|
CheckpointMetadata,
|
|
15
15
|
CheckpointTuple,
|
|
16
|
-
EmptyChannelError,
|
|
17
16
|
SerializerProtocol,
|
|
18
17
|
get_checkpoint_id,
|
|
19
18
|
)
|
|
@@ -32,7 +31,7 @@ _AIO_ERROR_MSG = (
|
|
|
32
31
|
)
|
|
33
32
|
|
|
34
33
|
|
|
35
|
-
class SqliteSaver(BaseCheckpointSaver):
|
|
34
|
+
class SqliteSaver(BaseCheckpointSaver[str]):
|
|
36
35
|
"""A checkpoint saver that stores checkpoints in a SQLite database.
|
|
37
36
|
|
|
38
37
|
Note:
|
|
@@ -104,10 +103,12 @@ class SqliteSaver(BaseCheckpointSaver):
|
|
|
104
103
|
with SqliteSaver.from_conn_string("checkpoints.sqlite") as memory:
|
|
105
104
|
...
|
|
106
105
|
"""
|
|
107
|
-
with
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
with closing(
|
|
107
|
+
sqlite3.connect(
|
|
108
|
+
conn_string,
|
|
109
|
+
# https://ricardoanderegg.com/posts/python-sqlite-thread-safety/
|
|
110
|
+
check_same_thread=False,
|
|
111
|
+
)
|
|
111
112
|
) as conn:
|
|
112
113
|
yield SqliteSaver(conn)
|
|
113
114
|
|
|
@@ -163,14 +164,15 @@ class SqliteSaver(BaseCheckpointSaver):
|
|
|
163
164
|
Yields:
|
|
164
165
|
sqlite3.Cursor: A cursor for the SQLite database.
|
|
165
166
|
"""
|
|
166
|
-
self.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
167
|
+
with self.lock:
|
|
168
|
+
self.setup()
|
|
169
|
+
cur = self.conn.cursor()
|
|
170
|
+
try:
|
|
171
|
+
yield cur
|
|
172
|
+
finally:
|
|
173
|
+
if transaction:
|
|
174
|
+
self.conn.commit()
|
|
175
|
+
cur.close()
|
|
174
176
|
|
|
175
177
|
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
|
176
178
|
"""Get a checkpoint tuple from the database.
|
|
@@ -396,7 +398,7 @@ class SqliteSaver(BaseCheckpointSaver):
|
|
|
396
398
|
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
|
397
399
|
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
|
|
398
400
|
serialized_metadata = self.jsonplus_serde.dumps(metadata)
|
|
399
|
-
with self.
|
|
401
|
+
with self.cursor() as cur:
|
|
400
402
|
cur.execute(
|
|
401
403
|
"INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
402
404
|
(
|
|
@@ -432,9 +434,14 @@ class SqliteSaver(BaseCheckpointSaver):
|
|
|
432
434
|
writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
|
|
433
435
|
task_id (str): Identifier for the task creating the writes.
|
|
434
436
|
"""
|
|
435
|
-
|
|
437
|
+
query = (
|
|
438
|
+
"INSERT OR REPLACE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
439
|
+
if all(w[0] in WRITES_IDX_MAP for w in writes)
|
|
440
|
+
else "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
441
|
+
)
|
|
442
|
+
with self.cursor() as cur:
|
|
436
443
|
cur.executemany(
|
|
437
|
-
|
|
444
|
+
query,
|
|
438
445
|
[
|
|
439
446
|
(
|
|
440
447
|
str(config["configurable"]["thread_id"]),
|
|
@@ -480,6 +487,7 @@ class SqliteSaver(BaseCheckpointSaver):
|
|
|
480
487
|
config: RunnableConfig,
|
|
481
488
|
checkpoint: Checkpoint,
|
|
482
489
|
metadata: CheckpointMetadata,
|
|
490
|
+
new_versions: ChannelVersions,
|
|
483
491
|
) -> RunnableConfig:
|
|
484
492
|
"""Save a checkpoint to the database asynchronously.
|
|
485
493
|
|
|
@@ -503,11 +511,10 @@ class SqliteSaver(BaseCheckpointSaver):
|
|
|
503
511
|
"""
|
|
504
512
|
if current is None:
|
|
505
513
|
current_v = 0
|
|
514
|
+
elif isinstance(current, int):
|
|
515
|
+
current_v = current
|
|
506
516
|
else:
|
|
507
517
|
current_v = int(current.split(".")[0])
|
|
508
518
|
next_v = current_v + 1
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
except EmptyChannelError:
|
|
512
|
-
next_h = ""
|
|
513
|
-
return f"{next_v:032}.{next_h}"
|
|
519
|
+
next_h = random.random()
|
|
520
|
+
return f"{next_v:032}.{next_h:016}"
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import random
|
|
2
3
|
from contextlib import asynccontextmanager
|
|
3
4
|
from typing import (
|
|
4
5
|
Any,
|
|
5
6
|
AsyncIterator,
|
|
7
|
+
Callable,
|
|
6
8
|
Dict,
|
|
7
9
|
Iterator,
|
|
8
|
-
List,
|
|
9
10
|
Optional,
|
|
10
11
|
Sequence,
|
|
11
12
|
Tuple,
|
|
@@ -26,12 +27,13 @@ from langgraph.checkpoint.base import (
|
|
|
26
27
|
get_checkpoint_id,
|
|
27
28
|
)
|
|
28
29
|
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
|
30
|
+
from langgraph.checkpoint.serde.types import ChannelProtocol
|
|
29
31
|
from langgraph.checkpoint.sqlite.utils import search_where
|
|
30
32
|
|
|
31
|
-
T = TypeVar("T", bound=
|
|
33
|
+
T = TypeVar("T", bound=Callable)
|
|
32
34
|
|
|
33
35
|
|
|
34
|
-
class AsyncSqliteSaver(BaseCheckpointSaver):
|
|
36
|
+
class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|
35
37
|
"""An asynchronous checkpoint saver that stores checkpoints in a SQLite database.
|
|
36
38
|
|
|
37
39
|
This class provides an asynchronous interface for saving and retrieving checkpoints
|
|
@@ -151,6 +153,18 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
|
|
151
153
|
Returns:
|
|
152
154
|
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
|
153
155
|
"""
|
|
156
|
+
try:
|
|
157
|
+
# check if we are in the main thread, only bg threads can block
|
|
158
|
+
# we don't check in other methods to avoid the overhead
|
|
159
|
+
if asyncio.get_running_loop() is self.loop:
|
|
160
|
+
raise asyncio.InvalidStateError(
|
|
161
|
+
"Synchronous calls to AsyncSqliteSaver are only allowed from a "
|
|
162
|
+
"different thread. From the main thread, use the async interface."
|
|
163
|
+
"For example, use `await checkpointer.aget_tuple(...)` or `await "
|
|
164
|
+
"graph.ainvoke(...)`."
|
|
165
|
+
)
|
|
166
|
+
except RuntimeError:
|
|
167
|
+
pass
|
|
154
168
|
return asyncio.run_coroutine_threadsafe(
|
|
155
169
|
self.aget_tuple(config), self.loop
|
|
156
170
|
).result()
|
|
@@ -181,7 +195,8 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
|
|
181
195
|
while True:
|
|
182
196
|
try:
|
|
183
197
|
yield asyncio.run_coroutine_threadsafe(
|
|
184
|
-
anext(aiter_),
|
|
198
|
+
anext(aiter_),
|
|
199
|
+
self.loop,
|
|
185
200
|
).result()
|
|
186
201
|
except StopAsyncIteration:
|
|
187
202
|
break
|
|
@@ -212,7 +227,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
|
|
212
227
|
).result()
|
|
213
228
|
|
|
214
229
|
def put_writes(
|
|
215
|
-
self, config: RunnableConfig, writes:
|
|
230
|
+
self, config: RunnableConfig, writes: Sequence[Tuple[str, Any]], task_id: str
|
|
216
231
|
) -> None:
|
|
217
232
|
return asyncio.run_coroutine_threadsafe(
|
|
218
233
|
self.aput_writes(config, writes, task_id), self.loop
|
|
@@ -276,7 +291,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
|
|
276
291
|
"""
|
|
277
292
|
await self.setup()
|
|
278
293
|
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
|
279
|
-
async with self.conn.cursor() as cur:
|
|
294
|
+
async with self.lock, self.conn.cursor() as cur:
|
|
280
295
|
# find the latest checkpoint for the thread_id
|
|
281
296
|
if checkpoint_id := get_checkpoint_id(config):
|
|
282
297
|
await cur.execute(
|
|
@@ -371,7 +386,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
|
|
371
386
|
ORDER BY checkpoint_id DESC"""
|
|
372
387
|
if limit:
|
|
373
388
|
query += f" LIMIT {limit}"
|
|
374
|
-
async with self.
|
|
389
|
+
async with self.lock, self.conn.execute(
|
|
390
|
+
query, params
|
|
391
|
+
) as cur, self.conn.cursor() as wcur:
|
|
375
392
|
async for (
|
|
376
393
|
thread_id,
|
|
377
394
|
checkpoint_ns,
|
|
@@ -438,7 +455,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
|
|
438
455
|
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
|
439
456
|
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
|
|
440
457
|
serialized_metadata = self.jsonplus_serde.dumps(metadata)
|
|
441
|
-
async with self.conn.execute(
|
|
458
|
+
async with self.lock, self.conn.execute(
|
|
442
459
|
"INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
443
460
|
(
|
|
444
461
|
str(config["configurable"]["thread_id"]),
|
|
@@ -474,10 +491,15 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
|
|
474
491
|
writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
|
|
475
492
|
task_id (str): Identifier for the task creating the writes.
|
|
476
493
|
"""
|
|
494
|
+
query = (
|
|
495
|
+
"INSERT OR REPLACE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
496
|
+
if all(w[0] in WRITES_IDX_MAP for w in writes)
|
|
497
|
+
else "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
498
|
+
)
|
|
477
499
|
await self.setup()
|
|
478
|
-
async with self.conn.cursor() as cur:
|
|
500
|
+
async with self.lock, self.conn.cursor() as cur:
|
|
479
501
|
await cur.executemany(
|
|
480
|
-
|
|
502
|
+
query,
|
|
481
503
|
[
|
|
482
504
|
(
|
|
483
505
|
str(config["configurable"]["thread_id"]),
|
|
@@ -491,3 +513,25 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
|
|
491
513
|
for idx, (channel, value) in enumerate(writes)
|
|
492
514
|
],
|
|
493
515
|
)
|
|
516
|
+
|
|
517
|
+
def get_next_version(self, current: Optional[str], channel: ChannelProtocol) -> str:
|
|
518
|
+
"""Generate the next version ID for a channel.
|
|
519
|
+
|
|
520
|
+
This method creates a new version identifier for a channel based on its current version.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
current (Optional[str]): The current version identifier of the channel.
|
|
524
|
+
channel (BaseChannel): The channel being versioned.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
str: The next version identifier, which is guaranteed to be monotonically increasing.
|
|
528
|
+
"""
|
|
529
|
+
if current is None:
|
|
530
|
+
current_v = 0
|
|
531
|
+
elif isinstance(current, int):
|
|
532
|
+
current_v = current
|
|
533
|
+
else:
|
|
534
|
+
current_v = int(current.split(".")[0])
|
|
535
|
+
next_v = current_v + 1
|
|
536
|
+
next_h = random.random()
|
|
537
|
+
return f"{next_v:032}.{next_h:016}"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "langgraph-checkpoint-sqlite"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.4"
|
|
4
4
|
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
|
5
5
|
authors = []
|
|
6
6
|
license = "MIT"
|
|
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
|
|
|
10
10
|
|
|
11
11
|
[tool.poetry.dependencies]
|
|
12
12
|
python = "^3.9.0"
|
|
13
|
-
langgraph-checkpoint = "^1.0.
|
|
13
|
+
langgraph-checkpoint = "^1.0.11"
|
|
14
14
|
aiosqlite = "^0.20.0"
|
|
15
15
|
|
|
16
16
|
[tool.poetry.group.dev.dependencies]
|
|
@@ -53,3 +53,13 @@ now = true
|
|
|
53
53
|
delay = 0.1
|
|
54
54
|
runner_args = ["--ff", "-v", "--tb", "short"]
|
|
55
55
|
patterns = ["*.py"]
|
|
56
|
+
|
|
57
|
+
[tool.mypy]
|
|
58
|
+
# https://mypy.readthedocs.io/en/stable/config_file.html
|
|
59
|
+
disallow_untyped_defs = "True"
|
|
60
|
+
explicit_package_bases = "True"
|
|
61
|
+
warn_no_return = "False"
|
|
62
|
+
warn_unused_ignores = "True"
|
|
63
|
+
warn_redundant_casts = "True"
|
|
64
|
+
allow_redefinition = "True"
|
|
65
|
+
disable_error_code = "typeddict-item, return-value"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|