langgraph-checkpoint-postgres 1.0.5__tar.gz → 1.0.7__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-postgres
3
- Version: 1.0.5
3
+ Version: 1.0.7
4
4
  Summary: Library with a Postgres implementation of LangGraph checkpoint saver.
5
5
  Home-page: https://www.github.com/langchain-ai/langgraph
6
6
  License: MIT
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3.9
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
- Requires-Dist: langgraph-checkpoint (>=1.0.1,<2.0.0)
14
+ Requires-Dist: langgraph-checkpoint (>=1.0.8,<2.0.0)
15
15
  Requires-Dist: orjson (>=3.10.1)
16
16
  Requires-Dist: psycopg (>=3.0.0,<4.0.0)
17
17
  Requires-Dist: psycopg-pool (>=3.0.0,<4.0.0)
@@ -10,6 +10,7 @@ from psycopg.types.json import Jsonb
10
10
  from psycopg_pool import ConnectionPool
11
11
 
12
12
  from langgraph.checkpoint.base import (
13
+ WRITES_IDX_MAP,
13
14
  ChannelVersions,
14
15
  Checkpoint,
15
16
  CheckpointMetadata,
@@ -337,9 +338,14 @@ class PostgresSaver(BasePostgresSaver):
337
338
  writes (List[Tuple[str, Any]]): List of writes to store.
338
339
  task_id (str): Identifier for the task creating the writes.
339
340
  """
341
+ query = (
342
+ self.UPSERT_CHECKPOINT_WRITES_SQL
343
+ if all(w[0] in WRITES_IDX_MAP for w in writes)
344
+ else self.INSERT_CHECKPOINT_WRITES_SQL
345
+ )
340
346
  with self._cursor(pipeline=True) as cur:
341
347
  cur.executemany(
342
- self.UPSERT_CHECKPOINT_WRITES_SQL,
348
+ query,
343
349
  self._dump_writes(
344
350
  config["configurable"]["thread_id"],
345
351
  config["configurable"]["checkpoint_ns"],
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
2
  from contextlib import asynccontextmanager
3
- from typing import Any, AsyncIterator, Optional, Union
3
+ from typing import Any, AsyncIterator, Iterator, List, Optional, Union
4
4
 
5
5
  from langchain_core.runnables import RunnableConfig
6
6
  from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline
@@ -10,6 +10,7 @@ from psycopg.types.json import Jsonb
10
10
  from psycopg_pool import AsyncConnectionPool
11
11
 
12
12
  from langgraph.checkpoint.base import (
13
+ WRITES_IDX_MAP,
13
14
  ChannelVersions,
14
15
  Checkpoint,
15
16
  CheckpointMetadata,
@@ -51,11 +52,16 @@ class AsyncPostgresSaver(BasePostgresSaver):
51
52
  self.conn = conn
52
53
  self.pipe = pipe
53
54
  self.lock = asyncio.Lock()
55
+ self.loop = asyncio.get_running_loop()
54
56
 
55
57
  @classmethod
56
58
  @asynccontextmanager
57
59
  async def from_conn_string(
58
- cls, conn_string: str, *, pipeline: bool = False
60
+ cls,
61
+ conn_string: str,
62
+ *,
63
+ pipeline: bool = False,
64
+ serde: Optional[SerializerProtocol] = None,
59
65
  ) -> AsyncIterator["AsyncPostgresSaver"]:
60
66
  """Create a new PostgresSaver instance from a connection string.
61
67
 
@@ -71,9 +77,9 @@ class AsyncPostgresSaver(BasePostgresSaver):
71
77
  ) as conn:
72
78
  if pipeline:
73
79
  async with conn.pipeline() as pipe:
74
- yield AsyncPostgresSaver(conn, pipe)
80
+ yield AsyncPostgresSaver(conn=conn, pipe=pipe, serde=serde)
75
81
  else:
76
- yield AsyncPostgresSaver(conn)
82
+ yield AsyncPostgresSaver(conn=conn, serde=serde)
77
83
 
78
84
  async def setup(self) -> None:
79
85
  """Set up the checkpoint database asynchronously.
@@ -291,18 +297,21 @@ class AsyncPostgresSaver(BasePostgresSaver):
291
297
  writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
292
298
  task_id (str): Identifier for the task creating the writes.
293
299
  """
300
+ query = (
301
+ self.UPSERT_CHECKPOINT_WRITES_SQL
302
+ if all(w[0] in WRITES_IDX_MAP for w in writes)
303
+ else self.INSERT_CHECKPOINT_WRITES_SQL
304
+ )
305
+ params = await asyncio.to_thread(
306
+ self._dump_writes,
307
+ config["configurable"]["thread_id"],
308
+ config["configurable"]["checkpoint_ns"],
309
+ config["configurable"]["checkpoint_id"],
310
+ task_id,
311
+ writes,
312
+ )
294
313
  async with self._cursor(pipeline=True) as cur:
295
- await cur.executemany(
296
- self.UPSERT_CHECKPOINT_WRITES_SQL,
297
- await asyncio.to_thread(
298
- self._dump_writes,
299
- config["configurable"]["thread_id"],
300
- config["configurable"]["checkpoint_ns"],
301
- config["configurable"]["checkpoint_id"],
302
- task_id,
303
- writes,
304
- ),
305
- )
314
+ await cur.executemany(query, params)
306
315
 
307
316
  @asynccontextmanager
308
317
  async def _cursor(self, *, pipeline: bool = False) -> AsyncIterator[AsyncCursor]:
@@ -329,3 +338,96 @@ class AsyncPostgresSaver(BasePostgresSaver):
329
338
  binary=True, row_factory=dict_row
330
339
  ) as cur:
331
340
  yield cur
341
+
342
+ def list(
343
+ self,
344
+ config: Optional[RunnableConfig],
345
+ *,
346
+ filter: Optional[dict[str, Any]] = None,
347
+ before: Optional[RunnableConfig] = None,
348
+ limit: Optional[int] = None,
349
+ ) -> Iterator[CheckpointTuple]:
350
+ """List checkpoints from the database.
351
+
352
+ This method retrieves a list of checkpoint tuples from the Postgres database based
353
+ on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
354
+
355
+ Args:
356
+ config (Optional[RunnableConfig]): Base configuration for filtering checkpoints.
357
+ filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata.
358
+ before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
359
+ limit (Optional[int]): Maximum number of checkpoints to return.
360
+
361
+ Yields:
362
+ Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
363
+ """
364
+ aiter_ = self.alist(config, filter=filter, before=before, limit=limit)
365
+ while True:
366
+ try:
367
+ yield asyncio.run_coroutine_threadsafe(
368
+ anext(aiter_), self.loop
369
+ ).result()
370
+ except StopAsyncIteration:
371
+ break
372
+
373
+ def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
374
+ """Get a checkpoint tuple from the database.
375
+
376
+ This method retrieves a checkpoint tuple from the Postgres database based on the
377
+ provided config. If the config contains a "checkpoint_id" key, the checkpoint with
378
+ the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
379
+ for the given thread ID is retrieved.
380
+
381
+ Args:
382
+ config (RunnableConfig): The config to use for retrieving the checkpoint.
383
+
384
+ Returns:
385
+ Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
386
+ """
387
+ return asyncio.run_coroutine_threadsafe(
388
+ self.aget_tuple(config), self.loop
389
+ ).result()
390
+
391
+ def put(
392
+ self,
393
+ config: RunnableConfig,
394
+ checkpoint: Checkpoint,
395
+ metadata: CheckpointMetadata,
396
+ new_versions: ChannelVersions,
397
+ ) -> RunnableConfig:
398
+ """Save a checkpoint to the database.
399
+
400
+ This method saves a checkpoint to the Postgres database. The checkpoint is associated
401
+ with the provided config and its parent config (if any).
402
+
403
+ Args:
404
+ config (RunnableConfig): The config to associate with the checkpoint.
405
+ checkpoint (Checkpoint): The checkpoint to save.
406
+ metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
407
+ new_versions (ChannelVersions): New channel versions as of this write.
408
+
409
+ Returns:
410
+ RunnableConfig: Updated configuration after storing the checkpoint.
411
+ """
412
+ return asyncio.run_coroutine_threadsafe(
413
+ self.aput(config, checkpoint, metadata, new_versions), self.loop
414
+ ).result()
415
+
416
+ def put_writes(
417
+ self,
418
+ config: RunnableConfig,
419
+ writes: List[tuple[str, Any]],
420
+ task_id: str,
421
+ ) -> None:
422
+ """Store intermediate writes linked to a checkpoint.
423
+
424
+ This method saves intermediate writes associated with a checkpoint to the database.
425
+
426
+ Args:
427
+ config (RunnableConfig): Configuration of the related checkpoint.
428
+ writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
429
+ task_id (str): Identifier for the task creating the writes.
430
+ """
431
+ return asyncio.run_coroutine_threadsafe(
432
+ self.aput_writes(config, writes, task_id), self.loop
433
+ ).result()
@@ -1,4 +1,4 @@
1
- from hashlib import md5
1
+ import random
2
2
  from typing import Any, List, Optional, Tuple
3
3
 
4
4
  from langchain_core.runnables import RunnableConfig
@@ -8,7 +8,6 @@ from langgraph.checkpoint.base import (
8
8
  WRITES_IDX_MAP,
9
9
  BaseCheckpointSaver,
10
10
  Checkpoint,
11
- EmptyChannelError,
12
11
  get_checkpoint_id,
13
12
  )
14
13
  from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
@@ -108,6 +107,15 @@ UPSERT_CHECKPOINTS_SQL = """
108
107
  """
109
108
 
110
109
  UPSERT_CHECKPOINT_WRITES_SQL = """
110
+ INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob)
111
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
112
+ ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) DO UPDATE SET
113
+ channel = EXCLUDED.channel,
114
+ type = EXCLUDED.type,
115
+ blob = EXCLUDED.blob;
116
+ """
117
+
118
+ INSERT_CHECKPOINT_WRITES_SQL = """
111
119
  INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob)
112
120
  VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
113
121
  ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) DO NOTHING
@@ -120,6 +128,7 @@ class BasePostgresSaver(BaseCheckpointSaver):
120
128
  UPSERT_CHECKPOINT_BLOBS_SQL = UPSERT_CHECKPOINT_BLOBS_SQL
121
129
  UPSERT_CHECKPOINTS_SQL = UPSERT_CHECKPOINTS_SQL
122
130
  UPSERT_CHECKPOINT_WRITES_SQL = UPSERT_CHECKPOINT_WRITES_SQL
131
+ INSERT_CHECKPOINT_WRITES_SQL = INSERT_CHECKPOINT_WRITES_SQL
123
132
 
124
133
  jsonplus_serde = JsonPlusSerializer()
125
134
 
@@ -217,13 +226,7 @@ class BasePostgresSaver(BaseCheckpointSaver):
217
226
  return self.jsonplus_serde.loads(self.jsonplus_serde.dumps(metadata))
218
227
 
219
228
  def _dump_metadata(self, metadata) -> str:
220
- serialized_metadata_type, serialized_metadata = self.jsonplus_serde.dumps_typed(
221
- metadata
222
- )
223
- if serialized_metadata_type != "json":
224
- raise TypeError(
225
- f"Failed to properly serialize metadata -- expected 'json', got '{serialized_metadata_type}'"
226
- )
229
+ serialized_metadata = self.jsonplus_serde.dumps(metadata)
227
230
  return serialized_metadata.decode()
228
231
 
229
232
  def get_next_version(self, current: Optional[str], channel: ChannelProtocol) -> str:
@@ -234,11 +237,8 @@ class BasePostgresSaver(BaseCheckpointSaver):
234
237
  else:
235
238
  current_v = int(current.split(".")[0])
236
239
  next_v = current_v + 1
237
- try:
238
- next_h = md5(self.serde.dumps_typed(channel.checkpoint())[1]).hexdigest()
239
- except EmptyChannelError:
240
- next_h = ""
241
- return f"{next_v:032}.{next_h}"
240
+ next_h = random.random()
241
+ return f"{next_v:032}.{next_h:016}"
242
242
 
243
243
  def _search_where(
244
244
  self,
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "langgraph-checkpoint-postgres"
3
- version = "1.0.5"
3
+ version = "1.0.7"
4
4
  description = "Library with a Postgres 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,<4.0"
13
- langgraph-checkpoint = "^1.0.1"
13
+ langgraph-checkpoint = "^1.0.8"
14
14
  orjson = ">=3.10.1"
15
15
  psycopg = "^3.0.0"
16
16
  psycopg-pool = "^3.0.0"