langgraph-checkpoint-sqlite 2.0.11__tar.gz → 3.0.0__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-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/PKG-INFO +3 -3
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/langgraph/checkpoint/sqlite/__init__.py +13 -16
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/langgraph/checkpoint/sqlite/aio.py +17 -20
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/langgraph/checkpoint/sqlite/utils.py +0 -1
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/langgraph/store/sqlite/aio.py +62 -21
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/langgraph/store/sqlite/base.py +64 -39
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/pyproject.toml +15 -7
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/tests/test_async_store.py +6 -6
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/tests/test_sqlite.py +12 -2
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/tests/test_store.py +36 -8
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/tests/test_ttl.py +76 -2
- langgraph_checkpoint_sqlite-3.0.0/uv.lock +1137 -0
- langgraph_checkpoint_sqlite-2.0.11/uv.lock +0 -1156
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/.gitignore +0 -0
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/Makefile +0 -0
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/README.md +0 -0
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/langgraph/cache/sqlite/__init__.py +0 -0
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/langgraph/checkpoint/sqlite/py.typed +0 -0
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/langgraph/store/sqlite/__init__.py +0 -0
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/tests/__init__.py +0 -0
- {langgraph_checkpoint_sqlite-2.0.11 → langgraph_checkpoint_sqlite-3.0.0}/tests/test_aiosqlite.py +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langgraph-checkpoint-sqlite
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Library with a SQLite implementation of LangGraph checkpoint saver.
|
|
5
5
|
Project-URL: Repository, https://www.github.com/langchain-ai/langgraph
|
|
6
6
|
License-Expression: MIT
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
8
|
Requires-Dist: aiosqlite>=0.20
|
|
9
|
-
Requires-Dist: langgraph-checkpoint<
|
|
9
|
+
Requires-Dist: langgraph-checkpoint<4.0.0,>=3
|
|
10
10
|
Requires-Dist: sqlite-vec>=0.1.6
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import random
|
|
4
5
|
import sqlite3
|
|
5
6
|
import threading
|
|
@@ -8,7 +9,6 @@ from contextlib import closing, contextmanager
|
|
|
8
9
|
from typing import Any, cast
|
|
9
10
|
|
|
10
11
|
from langchain_core.runnables import RunnableConfig
|
|
11
|
-
|
|
12
12
|
from langgraph.checkpoint.base import (
|
|
13
13
|
WRITES_IDX_MAP,
|
|
14
14
|
BaseCheckpointSaver,
|
|
@@ -21,6 +21,7 @@ from langgraph.checkpoint.base import (
|
|
|
21
21
|
get_checkpoint_metadata,
|
|
22
22
|
)
|
|
23
23
|
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
|
24
|
+
|
|
24
25
|
from langgraph.checkpoint.sqlite.utils import search_where
|
|
25
26
|
|
|
26
27
|
_AIO_ERROR_MSG = (
|
|
@@ -184,7 +185,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|
|
184
185
|
"""Get a checkpoint tuple from the database.
|
|
185
186
|
|
|
186
187
|
This method retrieves a checkpoint tuple from the SQLite database based on the
|
|
187
|
-
provided config. If the config contains a
|
|
188
|
+
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
|
188
189
|
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
|
|
189
190
|
for the given thread ID is retrieved.
|
|
190
191
|
|
|
@@ -192,7 +193,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|
|
192
193
|
config: The config to use for retrieving the checkpoint.
|
|
193
194
|
|
|
194
195
|
Returns:
|
|
195
|
-
|
|
196
|
+
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
|
196
197
|
|
|
197
198
|
Examples:
|
|
198
199
|
|
|
@@ -265,9 +266,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|
|
265
266
|
self.serde.loads_typed((type, checkpoint)),
|
|
266
267
|
cast(
|
|
267
268
|
CheckpointMetadata,
|
|
268
|
-
|
|
269
|
-
if metadata is not None
|
|
270
|
-
else {},
|
|
269
|
+
json.loads(metadata) if metadata is not None else {},
|
|
271
270
|
),
|
|
272
271
|
(
|
|
273
272
|
{
|
|
@@ -301,12 +300,12 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|
|
301
300
|
|
|
302
301
|
Args:
|
|
303
302
|
config: The config to use for listing the checkpoints.
|
|
304
|
-
filter: Additional filtering criteria for metadata.
|
|
305
|
-
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
|
306
|
-
limit: The maximum number of checkpoints to return.
|
|
303
|
+
filter: Additional filtering criteria for metadata.
|
|
304
|
+
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
|
305
|
+
limit: The maximum number of checkpoints to return.
|
|
307
306
|
|
|
308
307
|
Yields:
|
|
309
|
-
|
|
308
|
+
An iterator of checkpoint tuples.
|
|
310
309
|
|
|
311
310
|
Examples:
|
|
312
311
|
>>> from langgraph.checkpoint.sqlite import SqliteSaver
|
|
@@ -358,9 +357,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|
|
358
357
|
self.serde.loads_typed((type, checkpoint)),
|
|
359
358
|
cast(
|
|
360
359
|
CheckpointMetadata,
|
|
361
|
-
|
|
362
|
-
if metadata is not None
|
|
363
|
-
else {},
|
|
360
|
+
json.loads(metadata) if metadata is not None else {},
|
|
364
361
|
),
|
|
365
362
|
(
|
|
366
363
|
{
|
|
@@ -413,9 +410,9 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|
|
413
410
|
thread_id = config["configurable"]["thread_id"]
|
|
414
411
|
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
|
415
412
|
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
|
|
416
|
-
serialized_metadata =
|
|
417
|
-
get_checkpoint_metadata(config, metadata)
|
|
418
|
-
)
|
|
413
|
+
serialized_metadata = json.dumps(
|
|
414
|
+
get_checkpoint_metadata(config, metadata), ensure_ascii=False
|
|
415
|
+
).encode("utf-8", "ignore")
|
|
419
416
|
with self.cursor() as cur:
|
|
420
417
|
cur.execute(
|
|
421
418
|
"INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import json
|
|
4
5
|
import random
|
|
5
|
-
from collections.abc import AsyncIterator, Iterator, Sequence
|
|
6
|
+
from collections.abc import AsyncIterator, Callable, Iterator, Sequence
|
|
6
7
|
from contextlib import asynccontextmanager
|
|
7
|
-
from typing import Any,
|
|
8
|
+
from typing import Any, TypeVar, cast
|
|
8
9
|
|
|
9
10
|
import aiosqlite
|
|
10
11
|
from langchain_core.runnables import RunnableConfig
|
|
11
|
-
|
|
12
12
|
from langgraph.checkpoint.base import (
|
|
13
13
|
WRITES_IDX_MAP,
|
|
14
14
|
BaseCheckpointSaver,
|
|
@@ -21,6 +21,7 @@ from langgraph.checkpoint.base import (
|
|
|
21
21
|
get_checkpoint_metadata,
|
|
22
22
|
)
|
|
23
23
|
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
|
24
|
+
|
|
24
25
|
from langgraph.checkpoint.sqlite.utils import search_where
|
|
25
26
|
|
|
26
27
|
T = TypeVar("T", bound=Callable)
|
|
@@ -139,7 +140,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|
|
139
140
|
"""Get a checkpoint tuple from the database.
|
|
140
141
|
|
|
141
142
|
This method retrieves a checkpoint tuple from the SQLite database based on the
|
|
142
|
-
provided config. If the config contains a
|
|
143
|
+
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
|
143
144
|
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
|
|
144
145
|
for the given thread ID is retrieved.
|
|
145
146
|
|
|
@@ -147,7 +148,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|
|
147
148
|
config: The config to use for retrieving the checkpoint.
|
|
148
149
|
|
|
149
150
|
Returns:
|
|
150
|
-
|
|
151
|
+
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
|
151
152
|
"""
|
|
152
153
|
try:
|
|
153
154
|
# check if we are in the main thread, only bg threads can block
|
|
@@ -181,11 +182,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|
|
181
182
|
Args:
|
|
182
183
|
config: Base configuration for filtering checkpoints.
|
|
183
184
|
filter: Additional filtering criteria for metadata.
|
|
184
|
-
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
|
185
|
+
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
|
185
186
|
limit: Maximum number of checkpoints to return.
|
|
186
187
|
|
|
187
188
|
Yields:
|
|
188
|
-
|
|
189
|
+
An iterator of matching checkpoint tuples.
|
|
189
190
|
"""
|
|
190
191
|
try:
|
|
191
192
|
# check if we are in the main thread, only bg threads can block
|
|
@@ -316,7 +317,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|
|
316
317
|
"""Get a checkpoint tuple from the database asynchronously.
|
|
317
318
|
|
|
318
319
|
This method retrieves a checkpoint tuple from the SQLite database based on the
|
|
319
|
-
provided config. If the config contains a
|
|
320
|
+
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
|
320
321
|
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
|
|
321
322
|
for the given thread ID is retrieved.
|
|
322
323
|
|
|
@@ -324,7 +325,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|
|
324
325
|
config: The config to use for retrieving the checkpoint.
|
|
325
326
|
|
|
326
327
|
Returns:
|
|
327
|
-
|
|
328
|
+
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
|
328
329
|
"""
|
|
329
330
|
await self.setup()
|
|
330
331
|
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
|
@@ -377,9 +378,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|
|
377
378
|
self.serde.loads_typed((type, checkpoint)),
|
|
378
379
|
cast(
|
|
379
380
|
CheckpointMetadata,
|
|
380
|
-
|
|
381
|
-
if metadata is not None
|
|
382
|
-
else {},
|
|
381
|
+
(json.loads(metadata) if metadata is not None else {}),
|
|
383
382
|
),
|
|
384
383
|
(
|
|
385
384
|
{
|
|
@@ -414,11 +413,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|
|
414
413
|
Args:
|
|
415
414
|
config: Base configuration for filtering checkpoints.
|
|
416
415
|
filter: Additional filtering criteria for metadata.
|
|
417
|
-
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
|
416
|
+
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
|
418
417
|
limit: Maximum number of checkpoints to return.
|
|
419
418
|
|
|
420
419
|
Yields:
|
|
421
|
-
|
|
420
|
+
An asynchronous iterator of matching checkpoint tuples.
|
|
422
421
|
"""
|
|
423
422
|
await self.setup()
|
|
424
423
|
where, params = search_where(config, filter, before)
|
|
@@ -457,9 +456,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|
|
457
456
|
self.serde.loads_typed((type, checkpoint)),
|
|
458
457
|
cast(
|
|
459
458
|
CheckpointMetadata,
|
|
460
|
-
|
|
461
|
-
if metadata is not None
|
|
462
|
-
else {},
|
|
459
|
+
(json.loads(metadata) if metadata is not None else {}),
|
|
463
460
|
),
|
|
464
461
|
(
|
|
465
462
|
{
|
|
@@ -503,9 +500,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|
|
503
500
|
thread_id = config["configurable"]["thread_id"]
|
|
504
501
|
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
|
505
502
|
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
|
|
506
|
-
serialized_metadata =
|
|
507
|
-
get_checkpoint_metadata(config, metadata)
|
|
508
|
-
)
|
|
503
|
+
serialized_metadata = json.dumps(
|
|
504
|
+
get_checkpoint_metadata(config, metadata), ensure_ascii=False
|
|
505
|
+
).encode("utf-8", "ignore")
|
|
509
506
|
async with (
|
|
510
507
|
self.lock,
|
|
511
508
|
self.conn.execute(
|
|
@@ -3,15 +3,14 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
5
|
from collections import defaultdict
|
|
6
|
-
from collections.abc import AsyncIterator, Iterable, Sequence
|
|
6
|
+
from collections.abc import AsyncIterator, Callable, Iterable, Sequence
|
|
7
7
|
from contextlib import asynccontextmanager
|
|
8
8
|
from types import TracebackType
|
|
9
|
-
from typing import Any,
|
|
9
|
+
from typing import Any, cast
|
|
10
10
|
|
|
11
11
|
import aiosqlite
|
|
12
12
|
import orjson
|
|
13
13
|
import sqlite_vec # type: ignore[import-untyped]
|
|
14
|
-
|
|
15
14
|
from langgraph.store.base import (
|
|
16
15
|
GetOp,
|
|
17
16
|
ListNamespacesOp,
|
|
@@ -22,6 +21,7 @@ from langgraph.store.base import (
|
|
|
22
21
|
TTLConfig,
|
|
23
22
|
)
|
|
24
23
|
from langgraph.store.base.batch import AsyncBatchedBaseStore
|
|
24
|
+
|
|
25
25
|
from langgraph.store.sqlite.base import (
|
|
26
26
|
_PLACEHOLDER,
|
|
27
27
|
BaseSqliteStore,
|
|
@@ -303,7 +303,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|
|
303
303
|
|
|
304
304
|
Args:
|
|
305
305
|
timeout: Maximum time to wait for the task to stop, in seconds.
|
|
306
|
-
If None
|
|
306
|
+
If `None`, wait indefinitely.
|
|
307
307
|
|
|
308
308
|
Returns:
|
|
309
309
|
bool: True if the task was successfully stopped or wasn't running,
|
|
@@ -484,7 +484,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|
|
484
484
|
|
|
485
485
|
# Convert vectors to SQLite-friendly format
|
|
486
486
|
vector_params = []
|
|
487
|
-
for (ns, k, pathname, _), vector in zip(txt_params, vectors):
|
|
487
|
+
for (ns, k, pathname, _), vector in zip(txt_params, vectors, strict=False):
|
|
488
488
|
vector_params.extend(
|
|
489
489
|
[ns, k, pathname, sqlite_vec.serialize_float32(vector)]
|
|
490
490
|
)
|
|
@@ -507,7 +507,9 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|
|
507
507
|
results: List to store results in.
|
|
508
508
|
cur: Database cursor.
|
|
509
509
|
"""
|
|
510
|
-
|
|
510
|
+
prepared_queries, embedding_requests = self._prepare_batch_search_queries(
|
|
511
|
+
search_ops
|
|
512
|
+
)
|
|
511
513
|
|
|
512
514
|
# Setup dot_product function if it doesn't exist
|
|
513
515
|
if embedding_requests and self.embeddings:
|
|
@@ -515,23 +517,62 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|
|
515
517
|
[query for _, query in embedding_requests]
|
|
516
518
|
)
|
|
517
519
|
|
|
518
|
-
for (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
520
|
+
for (embed_req_idx, _), embedding in zip(
|
|
521
|
+
embedding_requests, vectors, strict=False
|
|
522
|
+
):
|
|
523
|
+
# Find the corresponding query in prepared_queries
|
|
524
|
+
# The embed_req_idx is the original index in search_ops, which should map to prepared_queries
|
|
525
|
+
if embed_req_idx < len(prepared_queries):
|
|
526
|
+
_params_list: list = prepared_queries[embed_req_idx][1]
|
|
527
|
+
for i, param in enumerate(_params_list):
|
|
528
|
+
if param is _PLACEHOLDER:
|
|
529
|
+
_params_list[i] = sqlite_vec.serialize_float32(embedding)
|
|
530
|
+
else:
|
|
531
|
+
logger.warning(
|
|
532
|
+
f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
|
|
533
|
+
)
|
|
523
534
|
|
|
524
|
-
for (
|
|
535
|
+
for (original_op_idx, _), (query, params, needs_refresh) in zip(
|
|
536
|
+
search_ops, prepared_queries, strict=False
|
|
537
|
+
):
|
|
525
538
|
await cur.execute(query, params)
|
|
526
539
|
rows = await cur.fetchall()
|
|
527
540
|
|
|
528
|
-
if
|
|
541
|
+
if needs_refresh and rows and self.ttl_config:
|
|
542
|
+
keys_to_refresh = []
|
|
543
|
+
for row_data in rows:
|
|
544
|
+
# Assuming row_data[0] is prefix (text), row_data[1] is key (text)
|
|
545
|
+
# These are raw text values directly from the DB.
|
|
546
|
+
keys_to_refresh.append((row_data[0], row_data[1]))
|
|
547
|
+
|
|
548
|
+
if keys_to_refresh:
|
|
549
|
+
updates_by_prefix = defaultdict(list)
|
|
550
|
+
for prefix_text, key_text in keys_to_refresh:
|
|
551
|
+
updates_by_prefix[prefix_text].append(key_text)
|
|
552
|
+
|
|
553
|
+
for prefix_text, key_list in updates_by_prefix.items():
|
|
554
|
+
placeholders = ",".join(["?"] * len(key_list))
|
|
555
|
+
update_query = f"""
|
|
556
|
+
UPDATE store
|
|
557
|
+
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
|
|
558
|
+
WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
|
|
559
|
+
"""
|
|
560
|
+
update_params = (prefix_text, *key_list)
|
|
561
|
+
try:
|
|
562
|
+
await cur.execute(update_query, update_params)
|
|
563
|
+
except Exception as e:
|
|
564
|
+
logger.error(
|
|
565
|
+
f"Error during TTL refresh update for search: {e}"
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# Process rows into items
|
|
569
|
+
if "score" in query: # Vector search query
|
|
529
570
|
items = [
|
|
530
571
|
_row_to_search_item(
|
|
531
|
-
_decode_ns_text(row[0]),
|
|
572
|
+
_decode_ns_text(row[0]), # prefix
|
|
532
573
|
{
|
|
533
|
-
"key": row[1],
|
|
534
|
-
"value": row[2],
|
|
574
|
+
"key": row[1], # key
|
|
575
|
+
"value": row[2], # value
|
|
535
576
|
"created_at": row[3],
|
|
536
577
|
"updated_at": row[4],
|
|
537
578
|
"expires_at": row[5] if len(row) > 5 else None,
|
|
@@ -545,10 +586,10 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|
|
545
586
|
else: # Regular search query
|
|
546
587
|
items = [
|
|
547
588
|
_row_to_search_item(
|
|
548
|
-
_decode_ns_text(row[0]),
|
|
589
|
+
_decode_ns_text(row[0]), # prefix
|
|
549
590
|
{
|
|
550
|
-
"key": row[1],
|
|
551
|
-
"value": row[2],
|
|
591
|
+
"key": row[1], # key
|
|
592
|
+
"value": row[2], # value
|
|
552
593
|
"created_at": row[3],
|
|
553
594
|
"updated_at": row[4],
|
|
554
595
|
"expires_at": row[5] if len(row) > 5 else None,
|
|
@@ -559,7 +600,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|
|
559
600
|
for row in rows
|
|
560
601
|
]
|
|
561
602
|
|
|
562
|
-
results[
|
|
603
|
+
results[original_op_idx] = items
|
|
563
604
|
|
|
564
605
|
async def _batch_list_namespaces_ops(
|
|
565
606
|
self,
|
|
@@ -575,7 +616,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|
|
575
616
|
cur: Database cursor.
|
|
576
617
|
"""
|
|
577
618
|
queries = self._get_batch_list_namespaces_queries(list_ops)
|
|
578
|
-
for (query, params), (idx, _) in zip(queries, list_ops):
|
|
619
|
+
for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
|
|
579
620
|
await cur.execute(query, params)
|
|
580
621
|
|
|
581
622
|
rows = await cur.fetchall()
|
|
@@ -7,13 +7,12 @@ import re
|
|
|
7
7
|
import sqlite3
|
|
8
8
|
import threading
|
|
9
9
|
from collections import defaultdict
|
|
10
|
-
from collections.abc import Iterable, Iterator, Sequence
|
|
10
|
+
from collections.abc import Callable, Iterable, Iterator, Sequence
|
|
11
11
|
from contextlib import contextmanager
|
|
12
|
-
from typing import Any,
|
|
12
|
+
from typing import Any, Literal, NamedTuple, cast
|
|
13
13
|
|
|
14
14
|
import orjson
|
|
15
15
|
import sqlite_vec # type: ignore[import-untyped]
|
|
16
|
-
|
|
17
16
|
from langgraph.store.base import (
|
|
18
17
|
BaseStore,
|
|
19
18
|
GetOp,
|
|
@@ -233,7 +232,7 @@ class BaseSqliteStore:
|
|
|
233
232
|
|
|
234
233
|
results = []
|
|
235
234
|
for namespace, items in namespace_groups.items():
|
|
236
|
-
_, keys = zip(*items)
|
|
235
|
+
_, keys = zip(*items, strict=False)
|
|
237
236
|
this_refresh_ttls = refresh_ttls[namespace]
|
|
238
237
|
refresh_ttl_any = any(this_refresh_ttls)
|
|
239
238
|
|
|
@@ -372,13 +371,15 @@ class BaseSqliteStore:
|
|
|
372
371
|
def _prepare_batch_search_queries(
|
|
373
372
|
self, search_ops: Sequence[tuple[int, SearchOp]]
|
|
374
373
|
) -> tuple[
|
|
375
|
-
list[
|
|
374
|
+
list[
|
|
375
|
+
tuple[str, list[None | str | list[float]], bool]
|
|
376
|
+
], # queries, params, needs_refresh
|
|
376
377
|
list[tuple[int, str]], # idx, query_text pairs to embed
|
|
377
378
|
]:
|
|
378
379
|
"""
|
|
379
|
-
Build per-SearchOp SQL queries (with optional TTL refresh) plus embedding requests.
|
|
380
|
+
Build per-SearchOp SQL queries (with optional TTL refresh flag) plus embedding requests.
|
|
380
381
|
Returns:
|
|
381
|
-
- queries: list of (SQL, param_list)
|
|
382
|
+
- queries: list of (SQL, param_list, needs_ttl_refresh_flag)
|
|
382
383
|
- embedding_requests: list of (original_index_in_search_ops, text_query)
|
|
383
384
|
"""
|
|
384
385
|
queries = []
|
|
@@ -519,30 +520,18 @@ class BaseSqliteStore:
|
|
|
519
520
|
logger.debug(f"Search query: {base_query}")
|
|
520
521
|
logger.debug(f"Search params: {params}")
|
|
521
522
|
|
|
522
|
-
#
|
|
523
|
-
|
|
523
|
+
# Determine if TTL refresh is needed
|
|
524
|
+
needs_ttl_refresh = bool(
|
|
524
525
|
op.refresh_ttl
|
|
525
526
|
and self.ttl_config
|
|
526
527
|
and self.ttl_config.get("refresh_on_read", False)
|
|
527
|
-
)
|
|
528
|
-
final_sql = f"""
|
|
529
|
-
WITH search_results AS (
|
|
530
|
-
{base_query}
|
|
531
|
-
),
|
|
532
|
-
updated AS (
|
|
533
|
-
UPDATE store
|
|
534
|
-
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
|
|
535
|
-
WHERE (prefix, key) IN (SELECT prefix, key FROM search_results)
|
|
536
|
-
AND ttl_minutes IS NOT NULL
|
|
537
|
-
)
|
|
538
|
-
SELECT * FROM search_results
|
|
539
|
-
"""
|
|
540
|
-
final_params = params[:] # copy params
|
|
541
|
-
else:
|
|
542
|
-
final_sql = base_query
|
|
543
|
-
final_params = params
|
|
528
|
+
)
|
|
544
529
|
|
|
545
|
-
|
|
530
|
+
# The base_query is now the final_sql, and we pass the refresh flag
|
|
531
|
+
final_sql = base_query
|
|
532
|
+
final_params = params
|
|
533
|
+
|
|
534
|
+
queries.append((final_sql, final_params, needs_ttl_refresh))
|
|
546
535
|
|
|
547
536
|
return queries, embedding_requests
|
|
548
537
|
|
|
@@ -840,7 +829,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|
|
840
829
|
|
|
841
830
|
results = []
|
|
842
831
|
for namespace, items in namespace_groups.items():
|
|
843
|
-
_, keys = zip(*items)
|
|
832
|
+
_, keys = zip(*items, strict=False)
|
|
844
833
|
this_refresh_ttls = refresh_ttls[namespace]
|
|
845
834
|
refresh_ttl_any = any(this_refresh_ttls)
|
|
846
835
|
|
|
@@ -1167,7 +1156,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|
|
1167
1156
|
|
|
1168
1157
|
Args:
|
|
1169
1158
|
timeout: Maximum time to wait for the thread to stop, in seconds.
|
|
1170
|
-
If None
|
|
1159
|
+
If `None`, wait indefinitely.
|
|
1171
1160
|
|
|
1172
1161
|
Returns:
|
|
1173
1162
|
bool: True if the thread was successfully stopped or wasn't running,
|
|
@@ -1315,7 +1304,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|
|
1315
1304
|
|
|
1316
1305
|
# Convert vectors to SQLite-friendly format
|
|
1317
1306
|
vector_params = []
|
|
1318
|
-
for (ns, k, pathname, _), vector in zip(txt_params, vectors):
|
|
1307
|
+
for (ns, k, pathname, _), vector in zip(txt_params, vectors, strict=False):
|
|
1319
1308
|
vector_params.extend(
|
|
1320
1309
|
[ns, k, pathname, sqlite_vec.serialize_float32(vector)]
|
|
1321
1310
|
)
|
|
@@ -1331,7 +1320,9 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|
|
1331
1320
|
results: list[Result],
|
|
1332
1321
|
cur: sqlite3.Cursor,
|
|
1333
1322
|
) -> None:
|
|
1334
|
-
|
|
1323
|
+
prepared_queries, embedding_requests = self._prepare_batch_search_queries(
|
|
1324
|
+
search_ops
|
|
1325
|
+
)
|
|
1335
1326
|
|
|
1336
1327
|
# Setup similarity functions if they don't exist
|
|
1337
1328
|
if embedding_requests and self.embeddings:
|
|
@@ -1341,16 +1332,50 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|
|
1341
1332
|
)
|
|
1342
1333
|
|
|
1343
1334
|
# Replace placeholders with actual embeddings
|
|
1344
|
-
for (
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1335
|
+
for (embed_req_idx, _), embedding in zip(
|
|
1336
|
+
embedding_requests, embeddings, strict=False
|
|
1337
|
+
):
|
|
1338
|
+
if embed_req_idx < len(prepared_queries):
|
|
1339
|
+
_params_list: list = prepared_queries[embed_req_idx][1]
|
|
1340
|
+
for i, param in enumerate(_params_list):
|
|
1341
|
+
if param is _PLACEHOLDER:
|
|
1342
|
+
_params_list[i] = sqlite_vec.serialize_float32(embedding)
|
|
1343
|
+
else:
|
|
1344
|
+
logger.warning(
|
|
1345
|
+
f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
|
|
1346
|
+
)
|
|
1349
1347
|
|
|
1350
|
-
for (
|
|
1348
|
+
for (original_op_idx, _), (query, params, needs_refresh) in zip(
|
|
1349
|
+
search_ops, prepared_queries, strict=False
|
|
1350
|
+
):
|
|
1351
1351
|
cur.execute(query, params)
|
|
1352
1352
|
rows = cur.fetchall()
|
|
1353
1353
|
|
|
1354
|
+
if needs_refresh and rows and self.ttl_config:
|
|
1355
|
+
keys_to_refresh = []
|
|
1356
|
+
for row_data in rows:
|
|
1357
|
+
keys_to_refresh.append((row_data[0], row_data[1]))
|
|
1358
|
+
|
|
1359
|
+
if keys_to_refresh:
|
|
1360
|
+
updates_by_prefix = defaultdict(list)
|
|
1361
|
+
for prefix_text, key_text in keys_to_refresh:
|
|
1362
|
+
updates_by_prefix[prefix_text].append(key_text)
|
|
1363
|
+
|
|
1364
|
+
for prefix_text, key_list in updates_by_prefix.items():
|
|
1365
|
+
placeholders = ",".join(["?"] * len(key_list))
|
|
1366
|
+
update_query = f"""
|
|
1367
|
+
UPDATE store
|
|
1368
|
+
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
|
|
1369
|
+
WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
|
|
1370
|
+
"""
|
|
1371
|
+
update_params = (prefix_text, *key_list)
|
|
1372
|
+
try:
|
|
1373
|
+
cur.execute(update_query, update_params)
|
|
1374
|
+
except Exception as e:
|
|
1375
|
+
logger.error(
|
|
1376
|
+
f"Error during TTL refresh update for search: {e}"
|
|
1377
|
+
)
|
|
1378
|
+
|
|
1354
1379
|
if "score" in query: # Vector search query
|
|
1355
1380
|
items = [
|
|
1356
1381
|
_row_to_search_item(
|
|
@@ -1385,7 +1410,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|
|
1385
1410
|
for row in rows
|
|
1386
1411
|
]
|
|
1387
1412
|
|
|
1388
|
-
results[
|
|
1413
|
+
results[original_op_idx] = items
|
|
1389
1414
|
|
|
1390
1415
|
def _batch_list_namespaces_ops(
|
|
1391
1416
|
self,
|
|
@@ -1394,7 +1419,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|
|
1394
1419
|
cur: sqlite3.Cursor,
|
|
1395
1420
|
) -> None:
|
|
1396
1421
|
queries = self._get_batch_list_namespaces_queries(list_ops)
|
|
1397
|
-
for (query, params), (idx, _) in zip(queries, list_ops):
|
|
1422
|
+
for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
|
|
1398
1423
|
cur.execute(query, params)
|
|
1399
1424
|
results[idx] = [_decode_ns_text(row[0]) for row in cur.fetchall()]
|
|
1400
1425
|
|
|
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "langgraph-checkpoint-sqlite"
|
|
7
|
-
version = "
|
|
7
|
+
version = "3.0.0"
|
|
8
8
|
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
|
9
9
|
authors = []
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
license = "MIT"
|
|
13
13
|
license-files = ['LICENSE']
|
|
14
14
|
dependencies = [
|
|
15
|
-
"langgraph-checkpoint>=
|
|
15
|
+
"langgraph-checkpoint>=3,<4.0.0",
|
|
16
16
|
"aiosqlite>=0.20",
|
|
17
17
|
"sqlite-vec>=0.1.6",
|
|
18
18
|
]
|
|
@@ -21,17 +21,23 @@ dependencies = [
|
|
|
21
21
|
Repository = "https://www.github.com/langchain-ai/langgraph"
|
|
22
22
|
|
|
23
23
|
[dependency-groups]
|
|
24
|
-
|
|
25
|
-
"ruff",
|
|
26
|
-
"codespell",
|
|
24
|
+
test = [
|
|
27
25
|
"pytest",
|
|
28
26
|
"pytest-asyncio",
|
|
29
27
|
"pytest-mock",
|
|
30
28
|
"pytest-watcher",
|
|
31
|
-
"mypy",
|
|
32
29
|
"langgraph-checkpoint",
|
|
33
30
|
"pytest-retry>=1.7.0",
|
|
34
31
|
]
|
|
32
|
+
lint = [
|
|
33
|
+
"ruff",
|
|
34
|
+
"codespell",
|
|
35
|
+
"mypy",
|
|
36
|
+
]
|
|
37
|
+
dev = [
|
|
38
|
+
{include-group = "test"},
|
|
39
|
+
{include-group = "lint"},
|
|
40
|
+
]
|
|
35
41
|
|
|
36
42
|
[tool.uv]
|
|
37
43
|
default-groups = ['dev']
|
|
@@ -53,8 +59,10 @@ lint.select = [
|
|
|
53
59
|
"UP", # pyupgrade
|
|
54
60
|
"B", # flake8-bugbear
|
|
55
61
|
"I", # isort
|
|
62
|
+
"UP", # pyupgrade
|
|
56
63
|
]
|
|
57
64
|
lint.ignore = ["E501", "B008"]
|
|
65
|
+
target-version = "py310"
|
|
58
66
|
|
|
59
67
|
[tool.pytest-watcher]
|
|
60
68
|
now = true
|