langgraph-checkpoint-postgres 1.0.3__tar.gz → 1.0.4__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.3 → langgraph_checkpoint_postgres-1.0.4}/PKG-INFO +7 -4
- {langgraph_checkpoint_postgres-1.0.3 → langgraph_checkpoint_postgres-1.0.4}/README.md +4 -2
- {langgraph_checkpoint_postgres-1.0.3 → langgraph_checkpoint_postgres-1.0.4}/langgraph/checkpoint/postgres/__init__.py +82 -60
- {langgraph_checkpoint_postgres-1.0.3 → langgraph_checkpoint_postgres-1.0.4}/langgraph/checkpoint/postgres/aio.py +89 -67
- {langgraph_checkpoint_postgres-1.0.3 → langgraph_checkpoint_postgres-1.0.4}/langgraph/checkpoint/postgres/base.py +10 -4
- {langgraph_checkpoint_postgres-1.0.3 → langgraph_checkpoint_postgres-1.0.4}/pyproject.toml +4 -3
- {langgraph_checkpoint_postgres-1.0.3 → langgraph_checkpoint_postgres-1.0.4}/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.4
|
|
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
|
|
@@ -13,7 +13,8 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Requires-Dist: langgraph-checkpoint (>=1.0.1,<2.0.0)
|
|
15
15
|
Requires-Dist: orjson (>=3.10.1)
|
|
16
|
-
Requires-Dist: psycopg
|
|
16
|
+
Requires-Dist: psycopg (>=3.0.0,<4.0.0)
|
|
17
|
+
Requires-Dist: psycopg-pool (>=3.0.0,<4.0.0)
|
|
17
18
|
Project-URL: Repository, https://www.github.com/langchain-ai/langgraph
|
|
18
19
|
Description-Content-Type: text/markdown
|
|
19
20
|
|
|
@@ -21,6 +22,10 @@ Description-Content-Type: text/markdown
|
|
|
21
22
|
|
|
22
23
|
Implementation of LangGraph CheckpointSaver that uses Postgres.
|
|
23
24
|
|
|
25
|
+
## Dependencies
|
|
26
|
+
|
|
27
|
+
By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) without any extras. However, you can choose a specific installation that best suits your needs [here](https://www.psycopg.org/psycopg3/docs/basic/install.html) (for example, `psycopg[binary]`).
|
|
28
|
+
|
|
24
29
|
## Usage
|
|
25
30
|
|
|
26
31
|
> [!IMPORTANT]
|
|
@@ -63,7 +68,6 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
|
|
63
68
|
}
|
|
64
69
|
},
|
|
65
70
|
"pending_sends": [],
|
|
66
|
-
"current_tasks": {}
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
# store checkpoint
|
|
@@ -106,7 +110,6 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
|
|
106
110
|
}
|
|
107
111
|
},
|
|
108
112
|
"pending_sends": [],
|
|
109
|
-
"current_tasks": {}
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
# store checkpoint
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Implementation of LangGraph CheckpointSaver that uses Postgres.
|
|
4
4
|
|
|
5
|
+
## Dependencies
|
|
6
|
+
|
|
7
|
+
By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) without any extras. However, you can choose a specific installation that best suits your needs [here](https://www.psycopg.org/psycopg3/docs/basic/install.html) (for example, `psycopg[binary]`).
|
|
8
|
+
|
|
5
9
|
## Usage
|
|
6
10
|
|
|
7
11
|
> [!IMPORTANT]
|
|
@@ -44,7 +48,6 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
|
|
44
48
|
}
|
|
45
49
|
},
|
|
46
50
|
"pending_sends": [],
|
|
47
|
-
"current_tasks": {}
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
# store checkpoint
|
|
@@ -87,7 +90,6 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
|
|
87
90
|
}
|
|
88
91
|
},
|
|
89
92
|
"pending_sends": [],
|
|
90
|
-
"current_tasks": {}
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
# store checkpoint
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import threading
|
|
2
2
|
from contextlib import contextmanager
|
|
3
|
-
from typing import Any, Iterator, List, Optional
|
|
3
|
+
from typing import Any, Iterator, List, Optional, Union
|
|
4
4
|
|
|
5
5
|
from langchain_core.runnables import RunnableConfig
|
|
6
6
|
from psycopg import Connection, Cursor, Pipeline
|
|
7
7
|
from psycopg.errors import UndefinedTable
|
|
8
8
|
from psycopg.rows import dict_row
|
|
9
9
|
from psycopg.types.json import Jsonb
|
|
10
|
+
from psycopg_pool import ConnectionPool
|
|
10
11
|
|
|
11
12
|
from langgraph.checkpoint.base import (
|
|
12
13
|
ChannelVersions,
|
|
@@ -21,16 +22,32 @@ from langgraph.checkpoint.postgres.base import (
|
|
|
21
22
|
from langgraph.checkpoint.serde.base import SerializerProtocol
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
@contextmanager
|
|
26
|
+
def _get_connection(conn: Union[Connection, ConnectionPool]) -> Iterator[Connection]:
|
|
27
|
+
if isinstance(conn, Connection):
|
|
28
|
+
yield conn
|
|
29
|
+
elif isinstance(conn, ConnectionPool):
|
|
30
|
+
with conn.connection() as conn:
|
|
31
|
+
yield conn
|
|
32
|
+
else:
|
|
33
|
+
raise TypeError(f"Invalid connection type: {type(conn)}")
|
|
34
|
+
|
|
35
|
+
|
|
24
36
|
class PostgresSaver(BasePostgresSaver):
|
|
25
37
|
lock: threading.Lock
|
|
26
38
|
|
|
27
39
|
def __init__(
|
|
28
40
|
self,
|
|
29
|
-
conn: Connection,
|
|
41
|
+
conn: Union[Connection, ConnectionPool],
|
|
30
42
|
pipe: Optional[Pipeline] = None,
|
|
31
43
|
serde: Optional[SerializerProtocol] = None,
|
|
32
44
|
) -> None:
|
|
33
45
|
super().__init__(serde=serde)
|
|
46
|
+
if isinstance(conn, ConnectionPool) and pipe is not None:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"Pipeline should be used only with a single Connection, not ConnectionPool."
|
|
49
|
+
)
|
|
50
|
+
|
|
34
51
|
self.conn = conn
|
|
35
52
|
self.pipe = pipe
|
|
36
53
|
self.lock = threading.Lock()
|
|
@@ -65,22 +82,21 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
65
82
|
already exist and runs database migrations. It MUST be called directly by the user
|
|
66
83
|
the first time checkpointer is used.
|
|
67
84
|
"""
|
|
68
|
-
with self.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
self.pipe.sync()
|
|
85
|
+
with self._cursor() as cur:
|
|
86
|
+
try:
|
|
87
|
+
version = cur.execute(
|
|
88
|
+
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
|
|
89
|
+
).fetchone()["v"]
|
|
90
|
+
except UndefinedTable:
|
|
91
|
+
version = -1
|
|
92
|
+
for v, migration in zip(
|
|
93
|
+
range(version + 1, len(self.MIGRATIONS)),
|
|
94
|
+
self.MIGRATIONS[version + 1 :],
|
|
95
|
+
):
|
|
96
|
+
cur.execute(migration)
|
|
97
|
+
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
|
98
|
+
if self.pipe:
|
|
99
|
+
self.pipe.sync()
|
|
84
100
|
|
|
85
101
|
def list(
|
|
86
102
|
self,
|
|
@@ -127,30 +143,33 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
127
143
|
if limit:
|
|
128
144
|
query += f" LIMIT {limit}"
|
|
129
145
|
# if we change this to use .stream() we need to make sure to close the cursor
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
"
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
"
|
|
146
|
-
|
|
147
|
-
"
|
|
148
|
-
|
|
146
|
+
with self._cursor() as cur:
|
|
147
|
+
cur.execute(query, args, binary=True)
|
|
148
|
+
for value in cur:
|
|
149
|
+
yield CheckpointTuple(
|
|
150
|
+
{
|
|
151
|
+
"configurable": {
|
|
152
|
+
"thread_id": value["thread_id"],
|
|
153
|
+
"checkpoint_ns": value["checkpoint_ns"],
|
|
154
|
+
"checkpoint_id": value["checkpoint_id"],
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
**self._load_checkpoint(value["checkpoint"]),
|
|
159
|
+
"channel_values": self._load_blobs(value["channel_values"]),
|
|
160
|
+
},
|
|
161
|
+
self._load_metadata(value["metadata"]),
|
|
162
|
+
{
|
|
163
|
+
"configurable": {
|
|
164
|
+
"thread_id": value["thread_id"],
|
|
165
|
+
"checkpoint_ns": value["checkpoint_ns"],
|
|
166
|
+
"checkpoint_id": value["parent_checkpoint_id"],
|
|
167
|
+
}
|
|
149
168
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
169
|
+
if value["parent_checkpoint_id"]
|
|
170
|
+
else None,
|
|
171
|
+
self._load_writes(value["pending_writes"]),
|
|
172
|
+
)
|
|
154
173
|
|
|
155
174
|
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
|
156
175
|
"""Get a checkpoint tuple from the database.
|
|
@@ -198,7 +217,7 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
198
217
|
where = "WHERE thread_id = %s AND checkpoint_ns = %s ORDER BY checkpoint_id DESC LIMIT 1"
|
|
199
218
|
|
|
200
219
|
with self._cursor() as cur:
|
|
201
|
-
cur
|
|
220
|
+
cur.execute(
|
|
202
221
|
self.SELECT_SQL + where,
|
|
203
222
|
args,
|
|
204
223
|
binary=True,
|
|
@@ -316,7 +335,7 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
316
335
|
writes (List[Tuple[str, Any]]): List of writes to store.
|
|
317
336
|
task_id (str): Identifier for the task creating the writes.
|
|
318
337
|
"""
|
|
319
|
-
with self._cursor() as cur:
|
|
338
|
+
with self._cursor(pipeline=True) as cur:
|
|
320
339
|
cur.executemany(
|
|
321
340
|
self.UPSERT_CHECKPOINT_WRITES_SQL,
|
|
322
341
|
self._dump_writes(
|
|
@@ -330,21 +349,24 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
330
349
|
|
|
331
350
|
@contextmanager
|
|
332
351
|
def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor]:
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
352
|
+
with _get_connection(self.conn) as conn:
|
|
353
|
+
if self.pipe:
|
|
354
|
+
# a connection in pipeline mode can be used concurrently
|
|
355
|
+
# in multiple threads/coroutines, but only one cursor can be
|
|
356
|
+
# used at a time
|
|
357
|
+
try:
|
|
358
|
+
with conn.cursor(binary=True, row_factory=dict_row) as cur:
|
|
359
|
+
yield cur
|
|
360
|
+
finally:
|
|
361
|
+
if pipeline:
|
|
362
|
+
self.pipe.sync()
|
|
363
|
+
elif pipeline:
|
|
364
|
+
# a connection not in pipeline mode can only be used by one
|
|
365
|
+
# thread/coroutine at a time, so we acquire a lock
|
|
366
|
+
with self.lock, conn.pipeline(), conn.cursor(
|
|
367
|
+
binary=True, row_factory=dict_row
|
|
368
|
+
) as cur:
|
|
369
|
+
yield cur
|
|
370
|
+
else:
|
|
371
|
+
with self.lock, conn.cursor(binary=True, row_factory=dict_row) as cur:
|
|
339
372
|
yield cur
|
|
340
|
-
finally:
|
|
341
|
-
if pipeline:
|
|
342
|
-
self.pipe.sync()
|
|
343
|
-
elif pipeline:
|
|
344
|
-
# a connection not in pipeline mode can only be used by one
|
|
345
|
-
# thread/coroutine at a time, so we acquire a lock
|
|
346
|
-
with self.lock, self.conn.pipeline(), self.conn.cursor(binary=True) as cur:
|
|
347
|
-
yield cur
|
|
348
|
-
else:
|
|
349
|
-
with self.lock, self.conn.cursor(binary=True) as cur:
|
|
350
|
-
yield cur
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from contextlib import asynccontextmanager
|
|
3
|
-
from typing import Any, AsyncIterator, Optional
|
|
3
|
+
from typing import Any, AsyncIterator, Optional, Union
|
|
4
4
|
|
|
5
5
|
from langchain_core.runnables import RunnableConfig
|
|
6
6
|
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline
|
|
7
7
|
from psycopg.errors import UndefinedTable
|
|
8
8
|
from psycopg.rows import dict_row
|
|
9
9
|
from psycopg.types.json import Jsonb
|
|
10
|
+
from psycopg_pool import AsyncConnectionPool
|
|
10
11
|
|
|
11
12
|
from langgraph.checkpoint.base import (
|
|
12
13
|
ChannelVersions,
|
|
@@ -19,16 +20,34 @@ from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
|
|
19
20
|
from langgraph.checkpoint.serde.base import SerializerProtocol
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
@asynccontextmanager
|
|
24
|
+
async def _get_connection(
|
|
25
|
+
conn: Union[AsyncConnection, AsyncConnectionPool],
|
|
26
|
+
) -> AsyncIterator[AsyncConnection]:
|
|
27
|
+
if isinstance(conn, AsyncConnection):
|
|
28
|
+
yield conn
|
|
29
|
+
elif isinstance(conn, AsyncConnectionPool):
|
|
30
|
+
async with conn.connection() as conn:
|
|
31
|
+
yield conn
|
|
32
|
+
else:
|
|
33
|
+
raise TypeError(f"Invalid connection type: {type(conn)}")
|
|
34
|
+
|
|
35
|
+
|
|
22
36
|
class AsyncPostgresSaver(BasePostgresSaver):
|
|
23
37
|
lock: asyncio.Lock
|
|
24
38
|
|
|
25
39
|
def __init__(
|
|
26
40
|
self,
|
|
27
|
-
conn: AsyncConnection,
|
|
41
|
+
conn: Union[AsyncConnection, AsyncConnectionPool],
|
|
28
42
|
pipe: Optional[AsyncPipeline] = None,
|
|
29
43
|
serde: Optional[SerializerProtocol] = None,
|
|
30
44
|
) -> None:
|
|
31
45
|
super().__init__(serde=serde)
|
|
46
|
+
if isinstance(conn, AsyncConnectionPool) and pipe is not None:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"Pipeline should be used only with a single AsyncConnection, not AsyncConnectionPool."
|
|
49
|
+
)
|
|
50
|
+
|
|
32
51
|
self.conn = conn
|
|
33
52
|
self.pipe = pipe
|
|
34
53
|
self.lock = asyncio.Lock()
|
|
@@ -63,25 +82,22 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
63
82
|
already exist and runs database migrations. It MUST be called directly by the user
|
|
64
83
|
the first time checkpointer is used.
|
|
65
84
|
"""
|
|
66
|
-
async with self.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
)
|
|
83
|
-
if self.pipe:
|
|
84
|
-
await self.pipe.sync()
|
|
85
|
+
async with self._cursor() as cur:
|
|
86
|
+
try:
|
|
87
|
+
results = await cur.execute(
|
|
88
|
+
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
|
|
89
|
+
)
|
|
90
|
+
version = (await results.fetchone())["v"]
|
|
91
|
+
except UndefinedTable:
|
|
92
|
+
version = -1
|
|
93
|
+
for v, migration in zip(
|
|
94
|
+
range(version + 1, len(self.MIGRATIONS)),
|
|
95
|
+
self.MIGRATIONS[version + 1 :],
|
|
96
|
+
):
|
|
97
|
+
await cur.execute(migration)
|
|
98
|
+
await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
|
99
|
+
if self.pipe:
|
|
100
|
+
await self.pipe.sync()
|
|
85
101
|
|
|
86
102
|
async def alist(
|
|
87
103
|
self,
|
|
@@ -110,32 +126,35 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
110
126
|
if limit:
|
|
111
127
|
query += f" LIMIT {limit}"
|
|
112
128
|
# if we change this to use .stream() we need to make sure to close the cursor
|
|
113
|
-
async
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
self.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
"
|
|
131
|
-
|
|
132
|
-
"
|
|
133
|
-
|
|
129
|
+
async with self._cursor() as cur:
|
|
130
|
+
await cur.execute(query, args, binary=True)
|
|
131
|
+
async for value in cur:
|
|
132
|
+
yield CheckpointTuple(
|
|
133
|
+
{
|
|
134
|
+
"configurable": {
|
|
135
|
+
"thread_id": value["thread_id"],
|
|
136
|
+
"checkpoint_ns": value["checkpoint_ns"],
|
|
137
|
+
"checkpoint_id": value["checkpoint_id"],
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
**self._load_checkpoint(value["checkpoint"]),
|
|
142
|
+
"channel_values": await asyncio.to_thread(
|
|
143
|
+
self._load_blobs, value["channel_values"]
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
self._load_metadata(value["metadata"]),
|
|
147
|
+
{
|
|
148
|
+
"configurable": {
|
|
149
|
+
"thread_id": value["thread_id"],
|
|
150
|
+
"checkpoint_ns": value["checkpoint_ns"],
|
|
151
|
+
"checkpoint_id": value["parent_checkpoint_id"],
|
|
152
|
+
}
|
|
134
153
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
154
|
+
if value["parent_checkpoint_id"]
|
|
155
|
+
else None,
|
|
156
|
+
await asyncio.to_thread(self._load_writes, value["pending_writes"]),
|
|
157
|
+
)
|
|
139
158
|
|
|
140
159
|
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
|
141
160
|
"""Get a checkpoint tuple from the database asynchronously.
|
|
@@ -162,7 +181,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
162
181
|
where = "WHERE thread_id = %s AND checkpoint_ns = %s ORDER BY checkpoint_id DESC LIMIT 1"
|
|
163
182
|
|
|
164
183
|
async with self._cursor() as cur:
|
|
165
|
-
|
|
184
|
+
await cur.execute(
|
|
166
185
|
self.SELECT_SQL + where,
|
|
167
186
|
args,
|
|
168
187
|
binary=True,
|
|
@@ -272,7 +291,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
272
291
|
writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
|
|
273
292
|
task_id (str): Identifier for the task creating the writes.
|
|
274
293
|
"""
|
|
275
|
-
async with self._cursor() as cur:
|
|
294
|
+
async with self._cursor(pipeline=True) as cur:
|
|
276
295
|
await cur.executemany(
|
|
277
296
|
self.UPSERT_CHECKPOINT_WRITES_SQL,
|
|
278
297
|
await asyncio.to_thread(
|
|
@@ -287,23 +306,26 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
287
306
|
|
|
288
307
|
@asynccontextmanager
|
|
289
308
|
async def _cursor(self, *, pipeline: bool = False) -> AsyncIterator[AsyncCursor]:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
309
|
+
async with _get_connection(self.conn) as conn:
|
|
310
|
+
if self.pipe:
|
|
311
|
+
# a connection in pipeline mode can be used concurrently
|
|
312
|
+
# in multiple threads/coroutines, but only one cursor can be
|
|
313
|
+
# used at a time
|
|
314
|
+
try:
|
|
315
|
+
async with conn.cursor(binary=True, row_factory=dict_row) as cur:
|
|
316
|
+
yield cur
|
|
317
|
+
finally:
|
|
318
|
+
if pipeline:
|
|
319
|
+
await self.pipe.sync()
|
|
320
|
+
elif pipeline:
|
|
321
|
+
# a connection not in pipeline mode can only be used by one
|
|
322
|
+
# thread/coroutine at a time, so we acquire a lock
|
|
323
|
+
async with self.lock, conn.pipeline(), conn.cursor(
|
|
324
|
+
binary=True, row_factory=dict_row
|
|
325
|
+
) as cur:
|
|
326
|
+
yield cur
|
|
327
|
+
else:
|
|
328
|
+
async with self.lock, conn.cursor(
|
|
329
|
+
binary=True, row_factory=dict_row
|
|
330
|
+
) as cur:
|
|
296
331
|
yield cur
|
|
297
|
-
finally:
|
|
298
|
-
if pipeline:
|
|
299
|
-
await self.pipe.sync()
|
|
300
|
-
elif pipeline:
|
|
301
|
-
# a connection not in pipeline mode can only be used by one
|
|
302
|
-
# thread/coroutine at a time, so we acquire a lock
|
|
303
|
-
async with self.lock, self.conn.pipeline(), self.conn.cursor(
|
|
304
|
-
binary=True
|
|
305
|
-
) as cur:
|
|
306
|
-
yield cur
|
|
307
|
-
else:
|
|
308
|
-
async with self.lock, self.conn.cursor(binary=True) as cur:
|
|
309
|
-
yield cur
|
|
@@ -6,6 +6,7 @@ from langchain_core.runnables import RunnableConfig
|
|
|
6
6
|
from psycopg.types.json import Jsonb
|
|
7
7
|
|
|
8
8
|
from langgraph.checkpoint.base import (
|
|
9
|
+
WRITES_IDX_MAP,
|
|
9
10
|
BaseCheckpointSaver,
|
|
10
11
|
Checkpoint,
|
|
11
12
|
EmptyChannelError,
|
|
@@ -112,6 +113,7 @@ class BasePostgresSaver(BaseCheckpointSaver):
|
|
|
112
113
|
UPSERT_CHECKPOINT_BLOBS_SQL = UPSERT_CHECKPOINT_BLOBS_SQL
|
|
113
114
|
UPSERT_CHECKPOINTS_SQL = UPSERT_CHECKPOINTS_SQL
|
|
114
115
|
UPSERT_CHECKPOINT_WRITES_SQL = UPSERT_CHECKPOINT_WRITES_SQL
|
|
116
|
+
|
|
115
117
|
jsonplus_serde = JsonPlusSerializer()
|
|
116
118
|
|
|
117
119
|
def _load_checkpoint(self, checkpoint: dict[str, Any]) -> Checkpoint:
|
|
@@ -199,7 +201,7 @@ class BasePostgresSaver(BaseCheckpointSaver):
|
|
|
199
201
|
checkpoint_ns,
|
|
200
202
|
checkpoint_id,
|
|
201
203
|
task_id,
|
|
202
|
-
idx,
|
|
204
|
+
WRITES_IDX_MAP.get(channel, idx),
|
|
203
205
|
channel,
|
|
204
206
|
*self.serde.dumps_typed(value),
|
|
205
207
|
)
|
|
@@ -253,9 +255,13 @@ class BasePostgresSaver(BaseCheckpointSaver):
|
|
|
253
255
|
if config:
|
|
254
256
|
wheres.append("thread_id = %s ")
|
|
255
257
|
param_values.append(config["configurable"]["thread_id"])
|
|
256
|
-
checkpoint_ns
|
|
257
|
-
|
|
258
|
-
|
|
258
|
+
if checkpoint_ns := config["configurable"].get("checkpoint_ns"):
|
|
259
|
+
wheres.append("checkpoint_ns = %s")
|
|
260
|
+
param_values.append(checkpoint_ns)
|
|
261
|
+
|
|
262
|
+
if checkpoint_id := get_checkpoint_id(config):
|
|
263
|
+
wheres.append("checkpoint_id = %s ")
|
|
264
|
+
param_values.append(checkpoint_id)
|
|
259
265
|
|
|
260
266
|
# construct predicate for metadata filter
|
|
261
267
|
if filter:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "langgraph-checkpoint-postgres"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.4"
|
|
4
4
|
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
|
5
5
|
authors = []
|
|
6
6
|
license = "MIT"
|
|
@@ -12,7 +12,8 @@ packages = [{ include = "langgraph" }]
|
|
|
12
12
|
python = "^3.9.0,<4.0"
|
|
13
13
|
langgraph-checkpoint = "^1.0.1"
|
|
14
14
|
orjson = ">=3.10.1"
|
|
15
|
-
psycopg =
|
|
15
|
+
psycopg = "^3.0.0"
|
|
16
|
+
psycopg-pool = "^3.0.0"
|
|
16
17
|
|
|
17
18
|
[tool.poetry.group.dev.dependencies]
|
|
18
19
|
ruff = "^0.1.4"
|
|
@@ -23,7 +24,7 @@ pytest-asyncio = "^0.21.1"
|
|
|
23
24
|
pytest-mock = "^3.11.1"
|
|
24
25
|
pytest-watch = "^4.2.0"
|
|
25
26
|
mypy = "^1.10.0"
|
|
26
|
-
psycopg
|
|
27
|
+
psycopg = {extras = ["binary"], version = ">=3.0.0"}
|
|
27
28
|
langgraph-checkpoint = {path = "../checkpoint", develop = true}
|
|
28
29
|
|
|
29
30
|
[tool.pytest.ini_options]
|
|
File without changes
|