langgraph-checkpoint-postgres 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.
- {langgraph_checkpoint_postgres-1.0.0 → langgraph_checkpoint_postgres-1.0.2}/PKG-INFO +6 -1
- {langgraph_checkpoint_postgres-1.0.0 → langgraph_checkpoint_postgres-1.0.2}/README.md +5 -0
- {langgraph_checkpoint_postgres-1.0.0 → langgraph_checkpoint_postgres-1.0.2}/langgraph/checkpoint/postgres/__init__.py +121 -26
- {langgraph_checkpoint_postgres-1.0.0 → langgraph_checkpoint_postgres-1.0.2}/langgraph/checkpoint/postgres/aio.py +76 -28
- {langgraph_checkpoint_postgres-1.0.0 → langgraph_checkpoint_postgres-1.0.2}/langgraph/checkpoint/postgres/base.py +63 -46
- {langgraph_checkpoint_postgres-1.0.0 → langgraph_checkpoint_postgres-1.0.2}/pyproject.toml +1 -1
- {langgraph_checkpoint_postgres-1.0.0 → langgraph_checkpoint_postgres-1.0.2}/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.2
|
|
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
|
|
@@ -23,6 +23,9 @@ Implementation of LangGraph CheckpointSaver that uses Postgres.
|
|
|
23
23
|
|
|
24
24
|
## Usage
|
|
25
25
|
|
|
26
|
+
> [!IMPORTANT]
|
|
27
|
+
> When using Postgres checkpointers for the first time, make sure to call `.setup()` method on them to create required tables. See example below.
|
|
28
|
+
|
|
26
29
|
```python
|
|
27
30
|
from langgraph.checkpoint.postgres import PostgresSaver
|
|
28
31
|
|
|
@@ -31,6 +34,8 @@ read_config = {"configurable": {"thread_id": "1"}}
|
|
|
31
34
|
|
|
32
35
|
DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
|
|
33
36
|
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
|
37
|
+
# call .setup() the first time you're using the checkpointer
|
|
38
|
+
checkpointer.setup()
|
|
34
39
|
checkpoint = {
|
|
35
40
|
"v": 1,
|
|
36
41
|
"ts": "2024-07-31T20:14:19.804150+00:00",
|
|
@@ -4,6 +4,9 @@ Implementation of LangGraph CheckpointSaver that uses Postgres.
|
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
7
|
+
> [!IMPORTANT]
|
|
8
|
+
> When using Postgres checkpointers for the first time, make sure to call `.setup()` method on them to create required tables. See example below.
|
|
9
|
+
|
|
7
10
|
```python
|
|
8
11
|
from langgraph.checkpoint.postgres import PostgresSaver
|
|
9
12
|
|
|
@@ -12,6 +15,8 @@ read_config = {"configurable": {"thread_id": "1"}}
|
|
|
12
15
|
|
|
13
16
|
DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
|
|
14
17
|
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
|
18
|
+
# call .setup() the first time you're using the checkpointer
|
|
19
|
+
checkpointer.setup()
|
|
15
20
|
checkpoint = {
|
|
16
21
|
"v": 1,
|
|
17
22
|
"ts": "2024-07-31T20:14:19.804150+00:00",
|
|
@@ -4,6 +4,7 @@ from typing import Any, Iterator, List, Optional
|
|
|
4
4
|
|
|
5
5
|
from langchain_core.runnables import RunnableConfig
|
|
6
6
|
from psycopg import Connection, Cursor, Pipeline
|
|
7
|
+
from psycopg.errors import UndefinedTable
|
|
7
8
|
from psycopg.rows import dict_row
|
|
8
9
|
from psycopg.types.json import Jsonb
|
|
9
10
|
|
|
@@ -23,8 +24,6 @@ from langgraph.checkpoint.serde.base import SerializerProtocol
|
|
|
23
24
|
class PostgresSaver(BasePostgresSaver):
|
|
24
25
|
lock: threading.Lock
|
|
25
26
|
|
|
26
|
-
is_setup: bool
|
|
27
|
-
|
|
28
27
|
def __init__(
|
|
29
28
|
self,
|
|
30
29
|
conn: Connection,
|
|
@@ -35,7 +34,6 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
35
34
|
self.conn = conn
|
|
36
35
|
self.pipe = pipe
|
|
37
36
|
self.lock = threading.Lock()
|
|
38
|
-
self.is_setup = False
|
|
39
37
|
|
|
40
38
|
@classmethod
|
|
41
39
|
@contextmanager
|
|
@@ -63,27 +61,27 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
63
61
|
def setup(self) -> None:
|
|
64
62
|
"""Set up the checkpoint database asynchronously.
|
|
65
63
|
|
|
66
|
-
This method creates the necessary tables in the
|
|
67
|
-
already exist
|
|
68
|
-
|
|
64
|
+
This method creates the necessary tables in the Postgres database if they don't
|
|
65
|
+
already exist and runs database migrations. It MUST be called directly by the user
|
|
66
|
+
the first time checkpointer is used.
|
|
69
67
|
"""
|
|
70
|
-
if self.is_setup:
|
|
71
|
-
return
|
|
72
68
|
with self.lock:
|
|
73
|
-
create_table_queries = [
|
|
74
|
-
self.CREATE_CHECKPOINTS_SQL,
|
|
75
|
-
self.CREATE_CHECKPOINT_BLOBS_SQL,
|
|
76
|
-
self.CREATE_CHECKPOINT_WRITES_SQL,
|
|
77
|
-
]
|
|
78
69
|
with self.conn.cursor(binary=True) as cur:
|
|
79
|
-
|
|
80
|
-
cur.execute(
|
|
81
|
-
|
|
70
|
+
try:
|
|
71
|
+
version = cur.execute(
|
|
72
|
+
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
|
|
73
|
+
).fetchone()["v"]
|
|
74
|
+
except UndefinedTable:
|
|
75
|
+
version = -1
|
|
76
|
+
for v, migration in zip(
|
|
77
|
+
range(version + 1, len(self.MIGRATIONS)),
|
|
78
|
+
self.MIGRATIONS[version + 1 :],
|
|
79
|
+
):
|
|
80
|
+
cur.execute(migration)
|
|
81
|
+
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
|
82
82
|
if self.pipe:
|
|
83
83
|
self.pipe.sync()
|
|
84
84
|
|
|
85
|
-
self.is_setup = True
|
|
86
|
-
|
|
87
85
|
def list(
|
|
88
86
|
self,
|
|
89
87
|
config: Optional[RunnableConfig],
|
|
@@ -92,7 +90,38 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
92
90
|
before: Optional[RunnableConfig] = None,
|
|
93
91
|
limit: Optional[int] = None,
|
|
94
92
|
) -> Iterator[CheckpointTuple]:
|
|
95
|
-
|
|
93
|
+
"""List checkpoints from the database.
|
|
94
|
+
|
|
95
|
+
This method retrieves a list of checkpoint tuples from the Postgres database based
|
|
96
|
+
on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
config (RunnableConfig): The config to use for listing the checkpoints.
|
|
100
|
+
filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata. Defaults to None.
|
|
101
|
+
before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
|
102
|
+
limit (Optional[int]): The maximum number of checkpoints to return. Defaults to None.
|
|
103
|
+
|
|
104
|
+
Yields:
|
|
105
|
+
Iterator[CheckpointTuple]: An iterator of checkpoint tuples.
|
|
106
|
+
|
|
107
|
+
Examples:
|
|
108
|
+
>>> from langgraph.checkpoint.postgres import PostgresSaver
|
|
109
|
+
>>> DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
|
|
110
|
+
>>> with PostgresSaver.from_conn_string(DB_URI) as memory:
|
|
111
|
+
... # Run a graph, then list the checkpoints
|
|
112
|
+
>>> config = {"configurable": {"thread_id": "1"}}
|
|
113
|
+
>>> checkpoints = list(memory.list(config, limit=2))
|
|
114
|
+
>>> print(checkpoints)
|
|
115
|
+
[CheckpointTuple(...), CheckpointTuple(...)]
|
|
116
|
+
|
|
117
|
+
>>> config = {"configurable": {"thread_id": "1"}}
|
|
118
|
+
>>> before = {"configurable": {"checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875"}}
|
|
119
|
+
>>> with PostgresSaver.from_conn_string(DB_URI) as memory:
|
|
120
|
+
... # Run a graph, then list the checkpoints
|
|
121
|
+
>>> checkpoints = list(memory.list(config, before=before))
|
|
122
|
+
>>> print(checkpoints)
|
|
123
|
+
[CheckpointTuple(...), ...]
|
|
124
|
+
"""
|
|
96
125
|
where, args = self._search_where(config, filter, before)
|
|
97
126
|
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
|
|
98
127
|
if limit:
|
|
@@ -111,7 +140,7 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
111
140
|
**self._load_checkpoint(value["checkpoint"]),
|
|
112
141
|
"channel_values": self._load_blobs(value["channel_values"]),
|
|
113
142
|
},
|
|
114
|
-
value["metadata"],
|
|
143
|
+
self._load_metadata(value["metadata"]),
|
|
115
144
|
{
|
|
116
145
|
"configurable": {
|
|
117
146
|
"thread_id": value["thread_id"],
|
|
@@ -124,7 +153,40 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
124
153
|
)
|
|
125
154
|
|
|
126
155
|
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
|
127
|
-
|
|
156
|
+
"""Get a checkpoint tuple from the database.
|
|
157
|
+
|
|
158
|
+
This method retrieves a checkpoint tuple from the Postgres database based on the
|
|
159
|
+
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
|
160
|
+
the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
|
|
161
|
+
for the given thread ID is retrieved.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
config (RunnableConfig): The config to use for retrieving the checkpoint.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
|
|
171
|
+
Basic:
|
|
172
|
+
>>> config = {"configurable": {"thread_id": "1"}}
|
|
173
|
+
>>> checkpoint_tuple = memory.get_tuple(config)
|
|
174
|
+
>>> print(checkpoint_tuple)
|
|
175
|
+
CheckpointTuple(...)
|
|
176
|
+
|
|
177
|
+
With timestamp:
|
|
178
|
+
|
|
179
|
+
>>> config = {
|
|
180
|
+
... "configurable": {
|
|
181
|
+
... "thread_id": "1",
|
|
182
|
+
... "checkpoint_ns": "",
|
|
183
|
+
... "checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875",
|
|
184
|
+
... }
|
|
185
|
+
... }
|
|
186
|
+
>>> checkpoint_tuple = memory.get_tuple(config)
|
|
187
|
+
>>> print(checkpoint_tuple)
|
|
188
|
+
CheckpointTuple(...)
|
|
189
|
+
""" # noqa
|
|
128
190
|
thread_id = config["configurable"]["thread_id"]
|
|
129
191
|
checkpoint_id = get_checkpoint_id(config)
|
|
130
192
|
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
|
@@ -155,7 +217,7 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
155
217
|
**self._load_checkpoint(value["checkpoint"]),
|
|
156
218
|
"channel_values": self._load_blobs(value["channel_values"]),
|
|
157
219
|
},
|
|
158
|
-
value["metadata"],
|
|
220
|
+
self._load_metadata(value["metadata"]),
|
|
159
221
|
{
|
|
160
222
|
"configurable": {
|
|
161
223
|
"thread_id": thread_id,
|
|
@@ -175,6 +237,31 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
175
237
|
metadata: CheckpointMetadata,
|
|
176
238
|
new_versions: ChannelVersions,
|
|
177
239
|
) -> RunnableConfig:
|
|
240
|
+
"""Save a checkpoint to the database.
|
|
241
|
+
|
|
242
|
+
This method saves a checkpoint to the Postgres database. The checkpoint is associated
|
|
243
|
+
with the provided config and its parent config (if any).
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
config (RunnableConfig): The config to associate with the checkpoint.
|
|
247
|
+
checkpoint (Checkpoint): The checkpoint to save.
|
|
248
|
+
metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
|
|
249
|
+
new_versions (ChannelVersions): New channel versions as of this write.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
RunnableConfig: Updated configuration after storing the checkpoint.
|
|
253
|
+
|
|
254
|
+
Examples:
|
|
255
|
+
|
|
256
|
+
>>> from langgraph.checkpoint.postgres import PostgresSaver
|
|
257
|
+
>>> DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
|
|
258
|
+
>>> with PostgresSaver.from_conn_string(DB_URI) as memory:
|
|
259
|
+
>>> config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
|
|
260
|
+
>>> checkpoint = {"ts": "2024-05-04T06:32:42.235444+00:00", "id": "1ef4f797-8335-6428-8001-8a1503f9b875", "data": {"key": "value"}}
|
|
261
|
+
>>> saved_config = memory.put(config, checkpoint, {"source": "input", "step": 1, "writes": {"key": "value"}}, {})
|
|
262
|
+
>>> print(saved_config)
|
|
263
|
+
{'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef4f797-8335-6428-8001-8a1503f9b875'}}
|
|
264
|
+
"""
|
|
178
265
|
configurable = config["configurable"].copy()
|
|
179
266
|
thread_id = configurable.pop("thread_id")
|
|
180
267
|
checkpoint_ns = configurable.pop("checkpoint_ns")
|
|
@@ -198,7 +285,6 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
198
285
|
thread_id,
|
|
199
286
|
checkpoint_ns,
|
|
200
287
|
copy.pop("channel_values"),
|
|
201
|
-
copy["channel_versions"],
|
|
202
288
|
new_versions,
|
|
203
289
|
),
|
|
204
290
|
)
|
|
@@ -210,7 +296,7 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
210
296
|
checkpoint["id"],
|
|
211
297
|
checkpoint_id,
|
|
212
298
|
Jsonb(self._dump_checkpoint(copy)),
|
|
213
|
-
|
|
299
|
+
self._dump_metadata(metadata),
|
|
214
300
|
),
|
|
215
301
|
)
|
|
216
302
|
return next_config
|
|
@@ -221,6 +307,15 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
221
307
|
writes: List[tuple[str, Any]],
|
|
222
308
|
task_id: str,
|
|
223
309
|
) -> None:
|
|
310
|
+
"""Store intermediate writes linked to a checkpoint.
|
|
311
|
+
|
|
312
|
+
This method saves intermediate writes associated with a checkpoint to the Postgres database.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
config (RunnableConfig): Configuration of the related checkpoint.
|
|
316
|
+
writes (List[Tuple[str, Any]]): List of writes to store.
|
|
317
|
+
task_id (str): Identifier for the task creating the writes.
|
|
318
|
+
"""
|
|
224
319
|
with self._cursor() as cur:
|
|
225
320
|
cur.executemany(
|
|
226
321
|
self.UPSERT_CHECKPOINT_WRITES_SQL,
|
|
@@ -235,7 +330,6 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
235
330
|
|
|
236
331
|
@contextmanager
|
|
237
332
|
def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor]:
|
|
238
|
-
self.setup()
|
|
239
333
|
if self.pipe:
|
|
240
334
|
# a connection in pipeline mode can be used concurrently
|
|
241
335
|
# in multiple threads/coroutines, but only one cursor can be
|
|
@@ -244,7 +338,8 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
244
338
|
with self.conn.cursor(binary=True) as cur:
|
|
245
339
|
yield cur
|
|
246
340
|
finally:
|
|
247
|
-
|
|
341
|
+
if pipeline:
|
|
342
|
+
self.pipe.sync()
|
|
248
343
|
elif pipeline:
|
|
249
344
|
# a connection not in pipeline mode can only be used by one
|
|
250
345
|
# thread/coroutine at a time, so we acquire a lock
|
|
@@ -4,6 +4,7 @@ from typing import Any, AsyncIterator, Optional
|
|
|
4
4
|
|
|
5
5
|
from langchain_core.runnables import RunnableConfig
|
|
6
6
|
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline
|
|
7
|
+
from psycopg.errors import UndefinedTable
|
|
7
8
|
from psycopg.rows import dict_row
|
|
8
9
|
from psycopg.types.json import Jsonb
|
|
9
10
|
|
|
@@ -21,8 +22,6 @@ from langgraph.checkpoint.serde.base import SerializerProtocol
|
|
|
21
22
|
class AsyncPostgresSaver(BasePostgresSaver):
|
|
22
23
|
lock: asyncio.Lock
|
|
23
24
|
|
|
24
|
-
is_setup: bool
|
|
25
|
-
|
|
26
25
|
def __init__(
|
|
27
26
|
self,
|
|
28
27
|
conn: AsyncConnection,
|
|
@@ -33,7 +32,6 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
33
32
|
self.conn = conn
|
|
34
33
|
self.pipe = pipe
|
|
35
34
|
self.lock = asyncio.Lock()
|
|
36
|
-
self.is_setup = False
|
|
37
35
|
|
|
38
36
|
@classmethod
|
|
39
37
|
@asynccontextmanager
|
|
@@ -61,27 +59,31 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
61
59
|
async def setup(self) -> None:
|
|
62
60
|
"""Set up the checkpoint database asynchronously.
|
|
63
61
|
|
|
64
|
-
This method creates the necessary tables in the
|
|
65
|
-
already exist
|
|
66
|
-
|
|
62
|
+
This method creates the necessary tables in the Postgres database if they don't
|
|
63
|
+
already exist and runs database migrations. It MUST be called directly by the user
|
|
64
|
+
the first time checkpointer is used.
|
|
67
65
|
"""
|
|
68
|
-
if self.is_setup:
|
|
69
|
-
return
|
|
70
66
|
async with self.lock:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
async with self.conn.cursor(binary=True) as cur:
|
|
68
|
+
try:
|
|
69
|
+
version = (
|
|
70
|
+
await cur.execute(
|
|
71
|
+
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
|
|
72
|
+
)
|
|
73
|
+
).fetchone()["v"]
|
|
74
|
+
except UndefinedTable:
|
|
75
|
+
version = -1
|
|
76
|
+
for v, migration in zip(
|
|
77
|
+
range(version + 1, len(self.MIGRATIONS)),
|
|
78
|
+
self.MIGRATIONS[version + 1 :],
|
|
79
|
+
):
|
|
80
|
+
await cur.execute(migration)
|
|
81
|
+
await cur.execute(
|
|
82
|
+
f"INSERT INTO checkpoint_migrations (v) VALUES ({v})"
|
|
83
|
+
)
|
|
80
84
|
if self.pipe:
|
|
81
85
|
await self.pipe.sync()
|
|
82
86
|
|
|
83
|
-
self.is_setup = True
|
|
84
|
-
|
|
85
87
|
async def alist(
|
|
86
88
|
self,
|
|
87
89
|
config: Optional[RunnableConfig],
|
|
@@ -90,7 +92,20 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
90
92
|
before: Optional[RunnableConfig] = None,
|
|
91
93
|
limit: Optional[int] = None,
|
|
92
94
|
) -> AsyncIterator[CheckpointTuple]:
|
|
93
|
-
|
|
95
|
+
"""List checkpoints from the database asynchronously.
|
|
96
|
+
|
|
97
|
+
This method retrieves a list of checkpoint tuples from the Postgres database based
|
|
98
|
+
on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
config (Optional[RunnableConfig]): Base configuration for filtering checkpoints.
|
|
102
|
+
filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata.
|
|
103
|
+
before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
|
104
|
+
limit (Optional[int]): Maximum number of checkpoints to return.
|
|
105
|
+
|
|
106
|
+
Yields:
|
|
107
|
+
AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples.
|
|
108
|
+
"""
|
|
94
109
|
where, args = self._search_where(config, filter, before)
|
|
95
110
|
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
|
|
96
111
|
if limit:
|
|
@@ -111,7 +126,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
111
126
|
self._load_blobs, value["channel_values"]
|
|
112
127
|
),
|
|
113
128
|
},
|
|
114
|
-
value["metadata"],
|
|
129
|
+
self._load_metadata(value["metadata"]),
|
|
115
130
|
{
|
|
116
131
|
"configurable": {
|
|
117
132
|
"thread_id": value["thread_id"],
|
|
@@ -124,7 +139,19 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
124
139
|
)
|
|
125
140
|
|
|
126
141
|
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
|
127
|
-
|
|
142
|
+
"""Get a checkpoint tuple from the database asynchronously.
|
|
143
|
+
|
|
144
|
+
This method retrieves a checkpoint tuple from the Postgres database based on the
|
|
145
|
+
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
|
146
|
+
the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
|
|
147
|
+
for the given thread ID is retrieved.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
config (RunnableConfig): The config to use for retrieving the checkpoint.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
|
154
|
+
"""
|
|
128
155
|
thread_id = config["configurable"]["thread_id"]
|
|
129
156
|
checkpoint_id = get_checkpoint_id(config)
|
|
130
157
|
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
|
@@ -157,7 +184,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
157
184
|
self._load_blobs, value["channel_values"]
|
|
158
185
|
),
|
|
159
186
|
},
|
|
160
|
-
value["metadata"],
|
|
187
|
+
self._load_metadata(value["metadata"]),
|
|
161
188
|
{
|
|
162
189
|
"configurable": {
|
|
163
190
|
"thread_id": thread_id,
|
|
@@ -177,7 +204,20 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
177
204
|
metadata: CheckpointMetadata,
|
|
178
205
|
new_versions: ChannelVersions,
|
|
179
206
|
) -> RunnableConfig:
|
|
180
|
-
|
|
207
|
+
"""Save a checkpoint to the database asynchronously.
|
|
208
|
+
|
|
209
|
+
This method saves a checkpoint to the Postgres database. The checkpoint is associated
|
|
210
|
+
with the provided config and its parent config (if any).
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
config (RunnableConfig): The config to associate with the checkpoint.
|
|
214
|
+
checkpoint (Checkpoint): The checkpoint to save.
|
|
215
|
+
metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
|
|
216
|
+
new_versions (ChannelVersions): New channel versions as of this write.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
RunnableConfig: Updated configuration after storing the checkpoint.
|
|
220
|
+
"""
|
|
181
221
|
configurable = config["configurable"].copy()
|
|
182
222
|
thread_id = configurable.pop("thread_id")
|
|
183
223
|
checkpoint_ns = configurable.pop("checkpoint_ns")
|
|
@@ -202,7 +242,6 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
202
242
|
thread_id,
|
|
203
243
|
checkpoint_ns,
|
|
204
244
|
copy.pop("channel_values"),
|
|
205
|
-
copy["channel_versions"],
|
|
206
245
|
new_versions,
|
|
207
246
|
),
|
|
208
247
|
)
|
|
@@ -214,7 +253,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
214
253
|
checkpoint["id"],
|
|
215
254
|
checkpoint_id,
|
|
216
255
|
Jsonb(self._dump_checkpoint(copy)),
|
|
217
|
-
|
|
256
|
+
self._dump_metadata(metadata),
|
|
218
257
|
),
|
|
219
258
|
)
|
|
220
259
|
return next_config
|
|
@@ -225,6 +264,15 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
225
264
|
writes: list[tuple[str, Any]],
|
|
226
265
|
task_id: str,
|
|
227
266
|
) -> None:
|
|
267
|
+
"""Store intermediate writes linked to a checkpoint asynchronously.
|
|
268
|
+
|
|
269
|
+
This method saves intermediate writes associated with a checkpoint to the database.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
config (RunnableConfig): Configuration of the related checkpoint.
|
|
273
|
+
writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
|
|
274
|
+
task_id (str): Identifier for the task creating the writes.
|
|
275
|
+
"""
|
|
228
276
|
async with self._cursor() as cur:
|
|
229
277
|
await cur.executemany(
|
|
230
278
|
self.UPSERT_CHECKPOINT_WRITES_SQL,
|
|
@@ -240,7 +288,6 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
240
288
|
|
|
241
289
|
@asynccontextmanager
|
|
242
290
|
async def _cursor(self, *, pipeline: bool = False) -> AsyncIterator[AsyncCursor]:
|
|
243
|
-
await self.setup()
|
|
244
291
|
if self.pipe:
|
|
245
292
|
# a connection in pipeline mode can be used concurrently
|
|
246
293
|
# in multiple threads/coroutines, but only one cursor can be
|
|
@@ -249,7 +296,8 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
249
296
|
async with self.conn.cursor(binary=True) as cur:
|
|
250
297
|
yield cur
|
|
251
298
|
finally:
|
|
252
|
-
|
|
299
|
+
if pipeline:
|
|
300
|
+
await self.pipe.sync()
|
|
253
301
|
elif pipeline:
|
|
254
302
|
# a connection not in pipeline mode can only be used by one
|
|
255
303
|
# thread/coroutine at a time, so we acquire a lock
|
|
@@ -11,10 +11,52 @@ from langgraph.checkpoint.base import (
|
|
|
11
11
|
EmptyChannelError,
|
|
12
12
|
get_checkpoint_id,
|
|
13
13
|
)
|
|
14
|
+
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
|
14
15
|
from langgraph.checkpoint.serde.types import ChannelProtocol
|
|
15
16
|
|
|
16
17
|
MetadataInput = Optional[dict[str, Any]]
|
|
17
18
|
|
|
19
|
+
"""
|
|
20
|
+
To add a new migration, add a new string to the MIGRATIONS list.
|
|
21
|
+
The position of the migration in the list is the version number.
|
|
22
|
+
"""
|
|
23
|
+
MIGRATIONS = [
|
|
24
|
+
"""CREATE TABLE IF NOT EXISTS checkpoint_migrations (
|
|
25
|
+
v INTEGER PRIMARY KEY
|
|
26
|
+
);""",
|
|
27
|
+
"""CREATE TABLE IF NOT EXISTS checkpoints (
|
|
28
|
+
thread_id TEXT NOT NULL,
|
|
29
|
+
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
|
30
|
+
checkpoint_id TEXT NOT NULL,
|
|
31
|
+
parent_checkpoint_id TEXT,
|
|
32
|
+
type TEXT,
|
|
33
|
+
checkpoint JSONB NOT NULL,
|
|
34
|
+
metadata JSONB NOT NULL DEFAULT '{}',
|
|
35
|
+
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
|
|
36
|
+
);""",
|
|
37
|
+
"""CREATE TABLE IF NOT EXISTS checkpoint_blobs (
|
|
38
|
+
thread_id TEXT NOT NULL,
|
|
39
|
+
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
|
40
|
+
channel TEXT NOT NULL,
|
|
41
|
+
version TEXT NOT NULL,
|
|
42
|
+
type TEXT NOT NULL,
|
|
43
|
+
blob BYTEA,
|
|
44
|
+
PRIMARY KEY (thread_id, checkpoint_ns, channel, version)
|
|
45
|
+
);""",
|
|
46
|
+
"""CREATE TABLE IF NOT EXISTS checkpoint_writes (
|
|
47
|
+
thread_id TEXT NOT NULL,
|
|
48
|
+
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
|
49
|
+
checkpoint_id TEXT NOT NULL,
|
|
50
|
+
task_id TEXT NOT NULL,
|
|
51
|
+
idx INTEGER NOT NULL,
|
|
52
|
+
channel TEXT NOT NULL,
|
|
53
|
+
type TEXT,
|
|
54
|
+
blob BYTEA NOT NULL,
|
|
55
|
+
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
|
|
56
|
+
);""",
|
|
57
|
+
"ALTER TABLE checkpoint_blobs ALTER COLUMN blob DROP not null;",
|
|
58
|
+
]
|
|
59
|
+
|
|
18
60
|
SELECT_SQL = """
|
|
19
61
|
select
|
|
20
62
|
thread_id,
|
|
@@ -42,43 +84,6 @@ select
|
|
|
42
84
|
) as pending_writes
|
|
43
85
|
from checkpoints """
|
|
44
86
|
|
|
45
|
-
CREATE_CHECKPOINTS_SQL = """
|
|
46
|
-
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
47
|
-
thread_id TEXT NOT NULL,
|
|
48
|
-
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
|
49
|
-
checkpoint_id TEXT NOT NULL,
|
|
50
|
-
parent_checkpoint_id TEXT,
|
|
51
|
-
type TEXT,
|
|
52
|
-
checkpoint JSONB NOT NULL,
|
|
53
|
-
metadata JSONB NOT NULL DEFAULT '{}',
|
|
54
|
-
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
|
|
55
|
-
);
|
|
56
|
-
"""
|
|
57
|
-
CREATE_CHECKPOINT_BLOBS_SQL = """
|
|
58
|
-
CREATE TABLE IF NOT EXISTS checkpoint_blobs (
|
|
59
|
-
thread_id TEXT NOT NULL,
|
|
60
|
-
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
|
61
|
-
channel TEXT NOT NULL,
|
|
62
|
-
version TEXT NOT NULL,
|
|
63
|
-
type TEXT NOT NULL,
|
|
64
|
-
blob BYTEA NOT NULL,
|
|
65
|
-
PRIMARY KEY (thread_id, checkpoint_ns, channel, version)
|
|
66
|
-
);"""
|
|
67
|
-
|
|
68
|
-
CREATE_CHECKPOINT_WRITES_SQL = """
|
|
69
|
-
CREATE TABLE IF NOT EXISTS checkpoint_writes (
|
|
70
|
-
thread_id TEXT NOT NULL,
|
|
71
|
-
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
|
72
|
-
checkpoint_id TEXT NOT NULL,
|
|
73
|
-
task_id TEXT NOT NULL,
|
|
74
|
-
idx INTEGER NOT NULL,
|
|
75
|
-
channel TEXT NOT NULL,
|
|
76
|
-
type TEXT,
|
|
77
|
-
blob BYTEA NOT NULL,
|
|
78
|
-
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
|
|
79
|
-
);
|
|
80
|
-
"""
|
|
81
|
-
|
|
82
87
|
UPSERT_CHECKPOINT_BLOBS_SQL = """
|
|
83
88
|
INSERT INTO checkpoint_blobs (thread_id, checkpoint_ns, channel, version, type, blob)
|
|
84
89
|
VALUES (%s, %s, %s, %s, %s, %s)
|
|
@@ -103,12 +108,11 @@ UPSERT_CHECKPOINT_WRITES_SQL = """
|
|
|
103
108
|
|
|
104
109
|
class BasePostgresSaver(BaseCheckpointSaver):
|
|
105
110
|
SELECT_SQL = SELECT_SQL
|
|
106
|
-
|
|
107
|
-
CREATE_CHECKPOINT_BLOBS_SQL = CREATE_CHECKPOINT_BLOBS_SQL
|
|
108
|
-
CREATE_CHECKPOINT_WRITES_SQL = CREATE_CHECKPOINT_WRITES_SQL
|
|
111
|
+
MIGRATIONS = MIGRATIONS
|
|
109
112
|
UPSERT_CHECKPOINT_BLOBS_SQL = UPSERT_CHECKPOINT_BLOBS_SQL
|
|
110
113
|
UPSERT_CHECKPOINTS_SQL = UPSERT_CHECKPOINTS_SQL
|
|
111
114
|
UPSERT_CHECKPOINT_WRITES_SQL = UPSERT_CHECKPOINT_WRITES_SQL
|
|
115
|
+
jsonplus_serde = JsonPlusSerializer()
|
|
112
116
|
|
|
113
117
|
def _load_checkpoint(self, checkpoint: dict[str, Any]) -> Checkpoint:
|
|
114
118
|
if len(checkpoint["pending_sends"]) == 2 and all(
|
|
@@ -137,6 +141,7 @@ class BasePostgresSaver(BaseCheckpointSaver):
|
|
|
137
141
|
return {
|
|
138
142
|
k.decode(): self.serde.loads_typed((t.decode(), v))
|
|
139
143
|
for k, t, v in blob_values
|
|
144
|
+
if t.decode() != "empty"
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
def _dump_blobs(
|
|
@@ -145,24 +150,23 @@ class BasePostgresSaver(BaseCheckpointSaver):
|
|
|
145
150
|
checkpoint_ns: str,
|
|
146
151
|
values: dict[str, Any],
|
|
147
152
|
versions: dict[str, str],
|
|
148
|
-
new_versions: Optional[dict[str, str]],
|
|
149
153
|
) -> list[tuple[str, str, str, str, str, bytes]]:
|
|
150
154
|
if not versions:
|
|
151
155
|
return []
|
|
152
156
|
|
|
153
|
-
if new_versions:
|
|
154
|
-
versions = new_versions
|
|
155
|
-
|
|
156
157
|
return [
|
|
157
158
|
(
|
|
158
159
|
thread_id,
|
|
159
160
|
checkpoint_ns,
|
|
160
161
|
k,
|
|
161
162
|
ver,
|
|
162
|
-
*
|
|
163
|
+
*(
|
|
164
|
+
self.serde.dumps_typed(values[k])
|
|
165
|
+
if k in values
|
|
166
|
+
else ("empty", None)
|
|
167
|
+
),
|
|
163
168
|
)
|
|
164
169
|
for k, ver in versions.items()
|
|
165
|
-
if k in values
|
|
166
170
|
]
|
|
167
171
|
|
|
168
172
|
def _load_writes(
|
|
@@ -202,6 +206,19 @@ class BasePostgresSaver(BaseCheckpointSaver):
|
|
|
202
206
|
for idx, (channel, value) in enumerate(writes)
|
|
203
207
|
]
|
|
204
208
|
|
|
209
|
+
def _load_metadata(self, metadata: dict[str, Any]) -> dict[str, Any]:
|
|
210
|
+
return self.jsonplus_serde.loads(self.jsonplus_serde.dumps(metadata))
|
|
211
|
+
|
|
212
|
+
def _dump_metadata(self, metadata) -> str:
|
|
213
|
+
serialized_metadata_type, serialized_metadata = self.jsonplus_serde.dumps_typed(
|
|
214
|
+
metadata
|
|
215
|
+
)
|
|
216
|
+
if serialized_metadata_type != "json":
|
|
217
|
+
raise TypeError(
|
|
218
|
+
f"Failed to properly serialize metadata -- expected 'json', got '{serialized_metadata_type}'"
|
|
219
|
+
)
|
|
220
|
+
return serialized_metadata.decode()
|
|
221
|
+
|
|
205
222
|
def get_next_version(self, current: Optional[str], channel: ChannelProtocol) -> str:
|
|
206
223
|
if current is None:
|
|
207
224
|
current_v = 0
|
|
File without changes
|