langgraph-checkpoint-sqlite 1.0.0__tar.gz → 1.0.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langgraph-checkpoint-sqlite
3
- Version: 1.0.0
3
+ Version: 1.0.2
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.1,<2.0.0)
15
+ Requires-Dist: langgraph-checkpoint (>=1.0.8,<2.0.0)
16
16
  Project-URL: Repository, https://www.github.com/langchain-ai/langgraph
17
17
  Description-Content-Type: text/markdown
18
18
 
@@ -53,7 +53,6 @@ with SqliteSaver.from_conn_string(":memory:") as checkpointer:
53
53
  }
54
54
  },
55
55
  "pending_sends": [],
56
- "current_tasks": {}
57
56
  }
58
57
 
59
58
  # store checkpoint
@@ -96,7 +95,6 @@ async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
96
95
  }
97
96
  },
98
97
  "pending_sends": [],
99
- "current_tasks": {}
100
98
  }
101
99
 
102
100
  # store checkpoint
@@ -108,3 +106,4 @@ async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
108
106
  # list checkpoints
109
107
  [c async for c in checkpointer.alist(read_config)]
110
108
  ```
109
+
@@ -35,7 +35,6 @@ with SqliteSaver.from_conn_string(":memory:") as checkpointer:
35
35
  }
36
36
  },
37
37
  "pending_sends": [],
38
- "current_tasks": {}
39
38
  }
40
39
 
41
40
  # store checkpoint
@@ -78,7 +77,6 @@ async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
78
77
  }
79
78
  },
80
79
  "pending_sends": [],
81
- "current_tasks": {}
82
80
  }
83
81
 
84
82
  # store checkpoint
@@ -89,4 +87,4 @@ async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
89
87
 
90
88
  # list checkpoints
91
89
  [c async for c in checkpointer.alist(read_config)]
92
- ```
90
+ ```
@@ -1,12 +1,13 @@
1
1
  import sqlite3
2
2
  import threading
3
- from contextlib import contextmanager
3
+ from contextlib import closing, contextmanager
4
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
8
8
 
9
9
  from langgraph.checkpoint.base import (
10
+ WRITES_IDX_MAP,
10
11
  BaseCheckpointSaver,
11
12
  ChannelVersions,
12
13
  Checkpoint,
@@ -176,7 +177,7 @@ class SqliteSaver(BaseCheckpointSaver):
176
177
 
177
178
  This method retrieves a checkpoint tuple from the SQLite database based on the
178
179
  provided config. If the config contains a "checkpoint_id" key, the checkpoint with
179
- the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
180
+ the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
180
181
  for the given thread ID is retrieved.
181
182
 
182
183
  Args:
@@ -193,12 +194,12 @@ class SqliteSaver(BaseCheckpointSaver):
193
194
  >>> print(checkpoint_tuple)
194
195
  CheckpointTuple(...)
195
196
 
196
- With timestamp:
197
+ With checkpoint ID:
197
198
 
198
199
  >>> config = {
199
200
  ... "configurable": {
200
201
  ... "thread_id": "1",
201
- ... "checkpoint_ns": "",
202
+ ... "checkpoint_ns": "",
202
203
  ... "checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875",
203
204
  ... }
204
205
  ... }
@@ -243,7 +244,7 @@ class SqliteSaver(BaseCheckpointSaver):
243
244
  }
244
245
  # find any pending writes
245
246
  cur.execute(
246
- "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?",
247
+ "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx",
247
248
  (
248
249
  str(config["configurable"]["thread_id"]),
249
250
  checkpoint_ns,
@@ -283,12 +284,12 @@ class SqliteSaver(BaseCheckpointSaver):
283
284
  """List checkpoints from the database.
284
285
 
285
286
  This method retrieves a list of checkpoint tuples from the SQLite database based
286
- on the provided config. The checkpoints are ordered by timestamp in descending order.
287
+ on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
287
288
 
288
289
  Args:
289
290
  config (RunnableConfig): The config to use for listing the checkpoints.
290
291
  filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata. Defaults to None.
291
- before (Optional[RunnableConfig]): If provided, only checkpoints before the specified timestamp are returned. Defaults to None.
292
+ before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
292
293
  limit (Optional[int]): The maximum number of checkpoints to return. Defaults to None.
293
294
 
294
295
  Yields:
@@ -318,7 +319,7 @@ class SqliteSaver(BaseCheckpointSaver):
318
319
  ORDER BY checkpoint_id DESC"""
319
320
  if limit:
320
321
  query += f" LIMIT {limit}"
321
- with self.cursor(transaction=False) as cur:
322
+ with self.cursor(transaction=False) as cur, closing(self.conn.cursor()) as wcur:
322
323
  cur.execute(query, param_values)
323
324
  for (
324
325
  thread_id,
@@ -329,6 +330,10 @@ class SqliteSaver(BaseCheckpointSaver):
329
330
  checkpoint,
330
331
  metadata,
331
332
  ) in cur:
333
+ wcur.execute(
334
+ "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx",
335
+ (thread_id, checkpoint_ns, checkpoint_id),
336
+ )
332
337
  yield CheckpointTuple(
333
338
  {
334
339
  "configurable": {
@@ -350,6 +355,10 @@ class SqliteSaver(BaseCheckpointSaver):
350
355
  if parent_checkpoint_id
351
356
  else None
352
357
  ),
358
+ [
359
+ (task_id, channel, self.serde.loads_typed((type, value)))
360
+ for task_id, channel, type, value in wcur
361
+ ],
353
362
  )
354
363
 
355
364
  def put(
@@ -367,10 +376,11 @@ class SqliteSaver(BaseCheckpointSaver):
367
376
  Args:
368
377
  config (RunnableConfig): The config to associate with the checkpoint.
369
378
  checkpoint (Checkpoint): The checkpoint to save.
370
- metadata (Optional[dict[str, Any]]): Additional metadata to save with the checkpoint. Defaults to None.
379
+ metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
380
+ new_versions (ChannelVersions): New channel versions as of this write.
371
381
 
372
382
  Returns:
373
- RunnableConfig: The updated config containing the saved checkpoint's timestamp.
383
+ RunnableConfig: Updated configuration after storing the checkpoint.
374
384
 
375
385
  Examples:
376
386
 
@@ -424,14 +434,14 @@ class SqliteSaver(BaseCheckpointSaver):
424
434
  """
425
435
  with self.lock, self.cursor() as cur:
426
436
  cur.executemany(
427
- "INSERT OR REPLACE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
437
+ "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
428
438
  [
429
439
  (
430
440
  str(config["configurable"]["thread_id"]),
431
441
  str(config["configurable"]["checkpoint_ns"]),
432
442
  str(config["configurable"]["checkpoint_id"]),
433
443
  task_id,
434
- idx,
444
+ WRITES_IDX_MAP.get(channel, idx),
435
445
  channel,
436
446
  *self.serde.dumps_typed(value),
437
447
  )
@@ -1,11 +1,11 @@
1
1
  import asyncio
2
- import functools
3
2
  from contextlib import asynccontextmanager
4
3
  from typing import (
5
4
  Any,
6
5
  AsyncIterator,
7
6
  Dict,
8
7
  Iterator,
8
+ List,
9
9
  Optional,
10
10
  Sequence,
11
11
  Tuple,
@@ -16,6 +16,7 @@ import aiosqlite
16
16
  from langchain_core.runnables import RunnableConfig
17
17
 
18
18
  from langgraph.checkpoint.base import (
19
+ WRITES_IDX_MAP,
19
20
  BaseCheckpointSaver,
20
21
  ChannelVersions,
21
22
  Checkpoint,
@@ -30,20 +31,6 @@ from langgraph.checkpoint.sqlite.utils import search_where
30
31
  T = TypeVar("T", bound=callable)
31
32
 
32
33
 
33
- def not_implemented_sync_method(func: T) -> T:
34
- @functools.wraps(func)
35
- def wrapper(*args, **kwargs):
36
- raise NotImplementedError(
37
- "The AsyncSqliteSaver does not support synchronous methods. "
38
- "Consider using the SqliteSaver instead.\n"
39
- "from langgraph.checkpoint.sqlite import SqliteSaver\n"
40
- "See https://langchain-ai.github.io/langgraph/reference/checkpoints/langgraph.checkpoint.sqlite.SqliteSaver "
41
- "for more information."
42
- )
43
-
44
- return wrapper
45
-
46
-
47
34
  class AsyncSqliteSaver(BaseCheckpointSaver):
48
35
  """An asynchronous checkpoint saver that stores checkpoints in a SQLite database.
49
36
 
@@ -131,6 +118,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
131
118
  self.jsonplus_serde = JsonPlusSerializer()
132
119
  self.conn = conn
133
120
  self.lock = asyncio.Lock()
121
+ self.loop = asyncio.get_running_loop()
134
122
  self.is_setup = False
135
123
 
136
124
  @classmethod
@@ -149,16 +137,24 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
149
137
  async with aiosqlite.connect(conn_string) as conn:
150
138
  yield AsyncSqliteSaver(conn)
151
139
 
152
- @not_implemented_sync_method
153
140
  def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
154
141
  """Get a checkpoint tuple from the database.
155
142
 
156
- Note:
157
- This method is not implemented for the AsyncSqliteSaver. Use `aget` instead.
158
- Or consider using the [SqliteSaver][sqlitesaver] checkpointer.
143
+ This method retrieves a checkpoint tuple from the SQLite database based on the
144
+ provided config. If the config contains a "checkpoint_id" key, the checkpoint with
145
+ the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
146
+ for the given thread ID is retrieved.
147
+
148
+ Args:
149
+ config (RunnableConfig): The config to use for retrieving the checkpoint.
150
+
151
+ Returns:
152
+ Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
159
153
  """
154
+ return asyncio.run_coroutine_threadsafe(
155
+ self.aget_tuple(config), self.loop
156
+ ).result()
160
157
 
161
- @not_implemented_sync_method
162
158
  def list(
163
159
  self,
164
160
  config: Optional[RunnableConfig],
@@ -167,21 +163,60 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
167
163
  before: Optional[RunnableConfig] = None,
168
164
  limit: Optional[int] = None,
169
165
  ) -> Iterator[CheckpointTuple]:
170
- """List checkpoints from the database.
166
+ """List checkpoints from the database asynchronously.
167
+
168
+ This method retrieves a list of checkpoint tuples from the SQLite database based
169
+ on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
170
+
171
+ Args:
172
+ config (Optional[RunnableConfig]): Base configuration for filtering checkpoints.
173
+ filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata.
174
+ before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
175
+ limit (Optional[int]): Maximum number of checkpoints to return.
171
176
 
172
- Note:
173
- This method is not implemented for the AsyncSqliteSaver. Use `alist` instead.
174
- Or consider using the [SqliteSaver][sqlitesaver] checkpointer.
177
+ Yields:
178
+ Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
175
179
  """
180
+ aiter_ = self.alist(config, filter=filter, before=before, limit=limit)
181
+ while True:
182
+ try:
183
+ yield asyncio.run_coroutine_threadsafe(
184
+ anext(aiter_), self.loop
185
+ ).result()
186
+ except StopAsyncIteration:
187
+ break
176
188
 
177
- @not_implemented_sync_method
178
189
  def put(
179
190
  self,
180
191
  config: RunnableConfig,
181
192
  checkpoint: Checkpoint,
182
193
  metadata: CheckpointMetadata,
194
+ new_versions: ChannelVersions,
183
195
  ) -> RunnableConfig:
184
- """Save a checkpoint to the database. FOO"""
196
+ """Save a checkpoint to the database.
197
+
198
+ This method saves a checkpoint to the SQLite database. The checkpoint is associated
199
+ with the provided config and its parent config (if any).
200
+
201
+ Args:
202
+ config (RunnableConfig): The config to associate with the checkpoint.
203
+ checkpoint (Checkpoint): The checkpoint to save.
204
+ metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
205
+ new_versions (ChannelVersions): New channel versions as of this write.
206
+
207
+ Returns:
208
+ RunnableConfig: Updated configuration after storing the checkpoint.
209
+ """
210
+ return asyncio.run_coroutine_threadsafe(
211
+ self.aput(config, checkpoint, metadata, new_versions), self.loop
212
+ ).result()
213
+
214
+ def put_writes(
215
+ self, config: RunnableConfig, writes: List[Tuple[str, Any]], task_id: str
216
+ ) -> None:
217
+ return asyncio.run_coroutine_threadsafe(
218
+ self.aput_writes(config, writes, task_id), self.loop
219
+ ).result()
185
220
 
186
221
  async def setup(self) -> None:
187
222
  """Set up the checkpoint database asynchronously.
@@ -230,7 +265,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
230
265
 
231
266
  This method retrieves a checkpoint tuple from the SQLite database based on the
232
267
  provided config. If the config contains a "checkpoint_id" key, the checkpoint with
233
- the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
268
+ the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
234
269
  for the given thread ID is retrieved.
235
270
 
236
271
  Args:
@@ -277,7 +312,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
277
312
  }
278
313
  # find any pending writes
279
314
  await cur.execute(
280
- "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?",
315
+ "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx",
281
316
  (
282
317
  str(config["configurable"]["thread_id"]),
283
318
  checkpoint_ns,
@@ -317,26 +352,26 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
317
352
  """List checkpoints from the database asynchronously.
318
353
 
319
354
  This method retrieves a list of checkpoint tuples from the SQLite database based
320
- on the provided config. The checkpoints are ordered by timestamp in descending order.
355
+ on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
321
356
 
322
357
  Args:
323
358
  config (Optional[RunnableConfig]): Base configuration for filtering checkpoints.
324
359
  filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata.
325
- before (Optional[RunnableConfig]): List checkpoints created before this configuration.
360
+ before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
326
361
  limit (Optional[int]): Maximum number of checkpoints to return.
327
362
 
328
363
  Yields:
329
364
  AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples.
330
365
  """
331
366
  await self.setup()
332
- where, param_values = search_where(config, filter, before)
367
+ where, params = search_where(config, filter, before)
333
368
  query = f"""SELECT thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata
334
369
  FROM checkpoints
335
370
  {where}
336
371
  ORDER BY checkpoint_id DESC"""
337
372
  if limit:
338
373
  query += f" LIMIT {limit}"
339
- async with self.conn.execute(query, param_values) as cursor:
374
+ async with self.conn.execute(query, params) as cur, self.conn.cursor() as wcur:
340
375
  async for (
341
376
  thread_id,
342
377
  checkpoint_ns,
@@ -345,7 +380,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
345
380
  type,
346
381
  checkpoint,
347
382
  metadata,
348
- ) in cursor:
383
+ ) in cur:
384
+ await wcur.execute(
385
+ "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx",
386
+ (thread_id, checkpoint_ns, checkpoint_id),
387
+ )
349
388
  yield CheckpointTuple(
350
389
  {
351
390
  "configurable": {
@@ -367,6 +406,10 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
367
406
  if parent_checkpoint_id
368
407
  else None
369
408
  ),
409
+ [
410
+ (task_id, channel, self.serde.loads_typed((type, value)))
411
+ async for task_id, channel, type, value in wcur
412
+ ],
370
413
  )
371
414
 
372
415
  async def aput(
@@ -385,10 +428,10 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
385
428
  config (RunnableConfig): The config to associate with the checkpoint.
386
429
  checkpoint (Checkpoint): The checkpoint to save.
387
430
  metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
388
- new_versions (dict): New versions as of this write
431
+ new_versions (ChannelVersions): New channel versions as of this write.
389
432
 
390
433
  Returns:
391
- RunnableConfig: The updated config containing the saved checkpoint's timestamp.
434
+ RunnableConfig: Updated configuration after storing the checkpoint.
392
435
  """
393
436
  await self.setup()
394
437
  thread_id = config["configurable"]["thread_id"]
@@ -432,19 +475,19 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
432
475
  task_id (str): Identifier for the task creating the writes.
433
476
  """
434
477
  await self.setup()
435
- async with self.conn.executemany(
436
- "INSERT OR REPLACE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
437
- [
438
- (
439
- str(config["configurable"]["thread_id"]),
440
- str(config["configurable"]["checkpoint_ns"]),
441
- str(config["configurable"]["checkpoint_id"]),
442
- task_id,
443
- idx,
444
- channel,
445
- *self.serde.dumps_typed(value),
446
- )
447
- for idx, (channel, value) in enumerate(writes)
448
- ],
449
- ):
450
- await self.conn.commit()
478
+ async with self.conn.cursor() as cur:
479
+ await cur.executemany(
480
+ "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
481
+ [
482
+ (
483
+ str(config["configurable"]["thread_id"]),
484
+ str(config["configurable"]["checkpoint_ns"]),
485
+ str(config["configurable"]["checkpoint_id"]),
486
+ task_id,
487
+ WRITES_IDX_MAP.get(channel, idx),
488
+ channel,
489
+ *self.serde.dumps_typed(value),
490
+ )
491
+ for idx, (channel, value) in enumerate(writes)
492
+ ],
493
+ )
@@ -70,9 +70,14 @@ def search_where(
70
70
  if config is not None:
71
71
  wheres.append("thread_id = ?")
72
72
  param_values.append(config["configurable"]["thread_id"])
73
- checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
74
- wheres.append("checkpoint_ns = ?")
75
- param_values.append(checkpoint_ns)
73
+ checkpoint_ns = config["configurable"].get("checkpoint_ns")
74
+ if checkpoint_ns is not None:
75
+ wheres.append("checkpoint_ns = ?")
76
+ param_values.append(checkpoint_ns)
77
+
78
+ if checkpoint_id := get_checkpoint_id(config):
79
+ wheres.append("checkpoint_id = ?")
80
+ param_values.append(checkpoint_id)
76
81
 
77
82
  # construct predicate for metadata filter
78
83
  if filter:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "langgraph-checkpoint-sqlite"
3
- version = "1.0.0"
3
+ version = "1.0.2"
4
4
  description = "Library with a SQLite implementation of LangGraph checkpoint saver."
5
5
  authors = []
6
6
  license = "MIT"
@@ -10,11 +10,11 @@ packages = [{ include = "langgraph" }]
10
10
 
11
11
  [tool.poetry.dependencies]
12
12
  python = "^3.9.0"
13
- langgraph-checkpoint = "^1.0.1"
13
+ langgraph-checkpoint = "^1.0.8"
14
14
  aiosqlite = "^0.20.0"
15
15
 
16
16
  [tool.poetry.group.dev.dependencies]
17
- ruff = "^0.1.4"
17
+ ruff = "^0.6.2"
18
18
  codespell = "^2.2.0"
19
19
  pytest = "^7.2.1"
20
20
  pytest-asyncio = "^0.21.1"