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.
- {langgraph_checkpoint_postgres-1.0.5 → langgraph_checkpoint_postgres-1.0.7}/PKG-INFO +2 -2
- {langgraph_checkpoint_postgres-1.0.5 → langgraph_checkpoint_postgres-1.0.7}/langgraph/checkpoint/postgres/__init__.py +7 -1
- {langgraph_checkpoint_postgres-1.0.5 → langgraph_checkpoint_postgres-1.0.7}/langgraph/checkpoint/postgres/aio.py +117 -15
- {langgraph_checkpoint_postgres-1.0.5 → langgraph_checkpoint_postgres-1.0.7}/langgraph/checkpoint/postgres/base.py +14 -14
- {langgraph_checkpoint_postgres-1.0.5 → langgraph_checkpoint_postgres-1.0.7}/pyproject.toml +2 -2
- {langgraph_checkpoint_postgres-1.0.5 → langgraph_checkpoint_postgres-1.0.7}/README.md +0 -0
- {langgraph_checkpoint_postgres-1.0.5 → langgraph_checkpoint_postgres-1.0.7}/langgraph/checkpoint/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: langgraph-checkpoint-postgres
|
|
3
|
-
Version: 1.0.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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.
|
|
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.
|
|
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"
|
|
File without changes
|
|
File without changes
|