langgraph-checkpoint-postgres 3.0.0__tar.gz → 3.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-3.0.2/LICENSE +21 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/PKG-INFO +6 -2
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/checkpoint/postgres/__init__.py +6 -4
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/checkpoint/postgres/aio.py +8 -4
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/checkpoint/postgres/base.py +1 -1
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/checkpoint/postgres/shallow.py +15 -9
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/store/postgres/aio.py +21 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/store/postgres/base.py +84 -20
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/pyproject.toml +5 -2
- langgraph_checkpoint_postgres-3.0.2/uv.lock +1346 -0
- langgraph_checkpoint_postgres-3.0.0/uv.lock +0 -1198
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/.gitignore +0 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/Makefile +0 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/README.md +0 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/checkpoint/postgres/_internal.py +0 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/checkpoint/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/store/postgres/__init__.py +0 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/langgraph/store/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/tests/__init__.py +0 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/tests/compose-postgres.yml +0 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/tests/conftest.py +0 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/tests/embed_test_utils.py +0 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/tests/test_async.py +0 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/tests/test_async_store.py +0 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/tests/test_store.py +0 -0
- {langgraph_checkpoint_postgres-3.0.0 → langgraph_checkpoint_postgres-3.0.2}/tests/test_sync.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 LangChain, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langgraph-checkpoint-postgres
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.2
|
|
4
4
|
Summary: Library with a Postgres implementation of LangGraph checkpoint saver.
|
|
5
|
-
Project-URL:
|
|
5
|
+
Project-URL: Source, https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres
|
|
6
|
+
Project-URL: Twitter, https://x.com/LangChainAI
|
|
7
|
+
Project-URL: Slack, https://www.langchain.com/join-community
|
|
8
|
+
Project-URL: Reddit, https://www.reddit.com/r/LangChain/
|
|
6
9
|
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
7
11
|
Requires-Python: >=3.10
|
|
8
12
|
Requires-Dist: langgraph-checkpoint<4.0.0,>=2.1.2
|
|
9
13
|
Requires-Dist: orjson>=3.10.1
|
|
@@ -97,7 +97,7 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
97
97
|
strict=False,
|
|
98
98
|
):
|
|
99
99
|
cur.execute(migration)
|
|
100
|
-
cur.execute(
|
|
100
|
+
cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
|
|
101
101
|
if self.pipe:
|
|
102
102
|
self.pipe.sync()
|
|
103
103
|
|
|
@@ -143,11 +143,13 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
143
143
|
"""
|
|
144
144
|
where, args = self._search_where(config, filter, before)
|
|
145
145
|
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
params = list(args)
|
|
147
|
+
if limit is not None:
|
|
148
|
+
query += " LIMIT %s"
|
|
149
|
+
params.append(int(limit))
|
|
148
150
|
# if we change this to use .stream() we need to make sure to close the cursor
|
|
149
151
|
with self._cursor() as cur:
|
|
150
|
-
cur.execute(query,
|
|
152
|
+
cur.execute(query, params)
|
|
151
153
|
values = cur.fetchall()
|
|
152
154
|
if not values:
|
|
153
155
|
return
|
|
@@ -102,7 +102,9 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
102
102
|
strict=False,
|
|
103
103
|
):
|
|
104
104
|
await cur.execute(migration)
|
|
105
|
-
await cur.execute(
|
|
105
|
+
await cur.execute(
|
|
106
|
+
"INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
|
|
107
|
+
)
|
|
106
108
|
if self.pipe:
|
|
107
109
|
await self.pipe.sync()
|
|
108
110
|
|
|
@@ -130,11 +132,13 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
130
132
|
"""
|
|
131
133
|
where, args = self._search_where(config, filter, before)
|
|
132
134
|
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
params = list(args)
|
|
136
|
+
if limit is not None:
|
|
137
|
+
query += " LIMIT %s"
|
|
138
|
+
params.append(int(limit))
|
|
135
139
|
# if we change this to use .stream() we need to make sure to close the cursor
|
|
136
140
|
async with self._cursor() as cur:
|
|
137
|
-
await cur.execute(query,
|
|
141
|
+
await cur.execute(query, params, binary=True)
|
|
138
142
|
values = await cur.fetchall()
|
|
139
143
|
if not values:
|
|
140
144
|
return
|
|
@@ -81,7 +81,7 @@ MIGRATIONS = [
|
|
|
81
81
|
"""
|
|
82
82
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
|
|
83
83
|
""",
|
|
84
|
-
"""ALTER TABLE checkpoint_writes ADD COLUMN task_path TEXT NOT NULL DEFAULT '';""",
|
|
84
|
+
"""ALTER TABLE checkpoint_writes ADD COLUMN IF NOT EXISTS task_path TEXT NOT NULL DEFAULT '';""",
|
|
85
85
|
]
|
|
86
86
|
|
|
87
87
|
SELECT_SQL = """
|
|
@@ -77,7 +77,7 @@ MIGRATIONS = [
|
|
|
77
77
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
|
|
78
78
|
""",
|
|
79
79
|
"""
|
|
80
|
-
ALTER TABLE checkpoint_writes ADD COLUMN task_path TEXT NOT NULL DEFAULT '';
|
|
80
|
+
ALTER TABLE checkpoint_writes ADD COLUMN IF NOT EXISTS task_path TEXT NOT NULL DEFAULT '';
|
|
81
81
|
""",
|
|
82
82
|
]
|
|
83
83
|
|
|
@@ -252,7 +252,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
|
|
252
252
|
strict=False,
|
|
253
253
|
):
|
|
254
254
|
cur.execute(migration)
|
|
255
|
-
cur.execute(
|
|
255
|
+
cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
|
|
256
256
|
if self.pipe:
|
|
257
257
|
self.pipe.sync()
|
|
258
258
|
|
|
@@ -272,10 +272,12 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
|
|
272
272
|
"""
|
|
273
273
|
where, args = self._search_where(config, filter, before)
|
|
274
274
|
query = self.SELECT_SQL + where
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
params = list(args)
|
|
276
|
+
if limit is not None:
|
|
277
|
+
query += " LIMIT %s"
|
|
278
|
+
params.append(int(limit))
|
|
277
279
|
with self._cursor() as cur:
|
|
278
|
-
cur.execute(
|
|
280
|
+
cur.execute(query, params, binary=True)
|
|
279
281
|
for value in cur:
|
|
280
282
|
checkpoint: Checkpoint = {
|
|
281
283
|
**value["checkpoint"],
|
|
@@ -614,7 +616,9 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
|
|
614
616
|
strict=False,
|
|
615
617
|
):
|
|
616
618
|
await cur.execute(migration)
|
|
617
|
-
await cur.execute(
|
|
619
|
+
await cur.execute(
|
|
620
|
+
"INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
|
|
621
|
+
)
|
|
618
622
|
if self.pipe:
|
|
619
623
|
await self.pipe.sync()
|
|
620
624
|
|
|
@@ -634,10 +638,12 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
|
|
634
638
|
"""
|
|
635
639
|
where, args = self._search_where(config, filter, before)
|
|
636
640
|
query = self.SELECT_SQL + where
|
|
637
|
-
|
|
638
|
-
|
|
641
|
+
params = list(args)
|
|
642
|
+
if limit is not None:
|
|
643
|
+
query += " LIMIT %s"
|
|
644
|
+
params.append(int(limit))
|
|
639
645
|
async with self._cursor() as cur:
|
|
640
|
-
await cur.execute(
|
|
646
|
+
await cur.execute(query, params, binary=True)
|
|
641
647
|
async for value in cur:
|
|
642
648
|
checkpoint: Checkpoint = {
|
|
643
649
|
**value["checkpoint"],
|
|
@@ -266,6 +266,27 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|
|
266
266
|
k: v(self) if v is not None and callable(v) else v
|
|
267
267
|
for k, v in migration.params.items()
|
|
268
268
|
}
|
|
269
|
+
if "dims" in params:
|
|
270
|
+
try:
|
|
271
|
+
params["dims"] = int(params["dims"])
|
|
272
|
+
except Exception as e:
|
|
273
|
+
raise ValueError(
|
|
274
|
+
f"Invalid dims for vector index: {params['dims']}"
|
|
275
|
+
) from e
|
|
276
|
+
if "vector_type" in params:
|
|
277
|
+
vt = str(params["vector_type"])
|
|
278
|
+
if vt not in ("vector", "halfvec"):
|
|
279
|
+
raise ValueError(
|
|
280
|
+
f"Invalid vector_type for pgvector: {vt}"
|
|
281
|
+
)
|
|
282
|
+
params["vector_type"] = vt
|
|
283
|
+
if "index_type" in params:
|
|
284
|
+
it = str(params["index_type"])
|
|
285
|
+
if it not in ("hnsw", "ivfflat"):
|
|
286
|
+
raise ValueError(
|
|
287
|
+
f"Invalid index_type for pgvector: {it}"
|
|
288
|
+
)
|
|
289
|
+
params["index_type"] = it
|
|
269
290
|
sql = sql % params
|
|
270
291
|
await cur.execute(sql)
|
|
271
292
|
await cur.execute(
|
|
@@ -91,7 +91,12 @@ WHERE expires_at IS NOT NULL;
|
|
|
91
91
|
VECTOR_MIGRATIONS: Sequence[Migration] = [
|
|
92
92
|
Migration(
|
|
93
93
|
"""
|
|
94
|
-
|
|
94
|
+
DO $$
|
|
95
|
+
BEGIN
|
|
96
|
+
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'vector') THEN
|
|
97
|
+
CREATE EXTENSION vector;
|
|
98
|
+
END IF;
|
|
99
|
+
END $$;
|
|
95
100
|
""",
|
|
96
101
|
),
|
|
97
102
|
Migration(
|
|
@@ -322,31 +327,36 @@ class BasePostgresStore(Generic[C]):
|
|
|
322
327
|
embedding_request: tuple[str, Sequence[tuple[str, str, str, str]]] | None = None
|
|
323
328
|
if inserts:
|
|
324
329
|
values = []
|
|
325
|
-
insertion_params = []
|
|
330
|
+
insertion_params: list[Any] = []
|
|
326
331
|
vector_values = []
|
|
327
332
|
embedding_request_params = []
|
|
328
333
|
# Handle TTL expiration
|
|
329
334
|
|
|
330
335
|
# First handle main store insertions
|
|
331
336
|
for op in inserts:
|
|
332
|
-
if op.ttl is not None:
|
|
333
|
-
expires_at_str = f"NOW() + INTERVAL '{op.ttl * 60} seconds'"
|
|
334
|
-
ttl_minutes = op.ttl
|
|
335
|
-
else:
|
|
336
|
-
expires_at_str = "NULL"
|
|
337
|
-
ttl_minutes = None
|
|
338
|
-
|
|
339
|
-
values.append(
|
|
340
|
-
f"(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, {expires_at_str}, %s)"
|
|
341
|
-
)
|
|
342
337
|
insertion_params.extend(
|
|
343
|
-
|
|
338
|
+
(
|
|
344
339
|
_namespace_to_text(op.namespace),
|
|
345
340
|
op.key,
|
|
346
341
|
Jsonb(cast(dict, op.value)),
|
|
347
|
-
|
|
348
|
-
]
|
|
342
|
+
)
|
|
349
343
|
)
|
|
344
|
+
if op.ttl is not None:
|
|
345
|
+
values.append(
|
|
346
|
+
"(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NOW() + %s::interval, %s)"
|
|
347
|
+
)
|
|
348
|
+
ttl_minutes = float(op.ttl)
|
|
349
|
+
insertion_params.extend(
|
|
350
|
+
(
|
|
351
|
+
f"{ttl_minutes * 60} seconds",
|
|
352
|
+
ttl_minutes,
|
|
353
|
+
)
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
values.append(
|
|
357
|
+
"(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, %s)"
|
|
358
|
+
)
|
|
359
|
+
insertion_params.append(None)
|
|
350
360
|
|
|
351
361
|
# Then handle embeddings if configured
|
|
352
362
|
if self.index_config:
|
|
@@ -460,6 +470,10 @@ class BasePostgresStore(Generic[C]):
|
|
|
460
470
|
cast(dict, self.index_config)["dims"],
|
|
461
471
|
)
|
|
462
472
|
else:
|
|
473
|
+
if vector_type not in ("vector", "halfvec"):
|
|
474
|
+
raise ValueError(
|
|
475
|
+
f"Invalid vector_type for pgvector: {vector_type}"
|
|
476
|
+
)
|
|
463
477
|
score_operator = score_operator % ("%s", vector_type)
|
|
464
478
|
|
|
465
479
|
vectors_per_doc_estimate = cast(dict, self.index_config)[
|
|
@@ -1117,6 +1131,27 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|
|
1117
1131
|
k: v(self) if v is not None and callable(v) else v
|
|
1118
1132
|
for k, v in migration.params.items()
|
|
1119
1133
|
}
|
|
1134
|
+
if "dims" in params:
|
|
1135
|
+
try:
|
|
1136
|
+
params["dims"] = int(params["dims"])
|
|
1137
|
+
except Exception as e:
|
|
1138
|
+
raise ValueError(
|
|
1139
|
+
f"Invalid dims for vector index: {params['dims']}"
|
|
1140
|
+
) from e
|
|
1141
|
+
if "vector_type" in params:
|
|
1142
|
+
vt = str(params["vector_type"])
|
|
1143
|
+
if vt not in ("vector", "halfvec"):
|
|
1144
|
+
raise ValueError(
|
|
1145
|
+
f"Invalid vector_type for pgvector: {vt}"
|
|
1146
|
+
)
|
|
1147
|
+
params["vector_type"] = vt
|
|
1148
|
+
if "index_type" in params:
|
|
1149
|
+
it = str(params["index_type"])
|
|
1150
|
+
if it not in ("hnsw", "ivfflat"):
|
|
1151
|
+
raise ValueError(
|
|
1152
|
+
f"Invalid index_type for pgvector: {it}"
|
|
1153
|
+
)
|
|
1154
|
+
params["index_type"] = it
|
|
1120
1155
|
sql = sql % params
|
|
1121
1156
|
cur.execute(sql)
|
|
1122
1157
|
cur.execute("INSERT INTO vector_migrations (v) VALUES (%s)", (v,))
|
|
@@ -1170,15 +1205,44 @@ def _get_vector_type_ops(store: BasePostgresStore) -> str:
|
|
|
1170
1205
|
|
|
1171
1206
|
|
|
1172
1207
|
def _get_index_params(store: Any) -> tuple[str, dict[str, Any]]:
|
|
1173
|
-
"""Get
|
|
1208
|
+
"""Get a sanitized index type and configuration based on config.
|
|
1209
|
+
|
|
1210
|
+
Only allow known-safe kinds and integer parameters to avoid SQL injection
|
|
1211
|
+
when constructing DDL strings for index creation.
|
|
1212
|
+
"""
|
|
1174
1213
|
if not store.index_config:
|
|
1175
1214
|
return "hnsw", {}
|
|
1176
1215
|
|
|
1177
1216
|
config = cast(PostgresIndexConfig, store.index_config)
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1217
|
+
raw = config.get("ann_index_config", _DEFAULT_ANN_CONFIG).copy()
|
|
1218
|
+
|
|
1219
|
+
kind = str(raw.pop("kind", "hnsw"))
|
|
1220
|
+
if kind not in ("hnsw", "ivfflat", "flat"):
|
|
1221
|
+
raise ValueError(
|
|
1222
|
+
f"Invalid index kind for pgvector: {kind}. Expected 'hnsw', 'ivfflat', or 'flat'."
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
raw.pop("vector_type", None)
|
|
1226
|
+
|
|
1227
|
+
if kind == "hnsw":
|
|
1228
|
+
allowed_keys = {"m", "ef_construction"}
|
|
1229
|
+
else: # ivfflat/flat
|
|
1230
|
+
allowed_keys = {"lists", "nlist"}
|
|
1231
|
+
|
|
1232
|
+
sanitized: dict[str, int] = {}
|
|
1233
|
+
for k, v in list(raw.items()):
|
|
1234
|
+
if k not in allowed_keys:
|
|
1235
|
+
continue
|
|
1236
|
+
key = "lists" if k == "nlist" else k
|
|
1237
|
+
try:
|
|
1238
|
+
ivalue = int(v) # type: ignore[call-overload]
|
|
1239
|
+
except Exception as e:
|
|
1240
|
+
raise ValueError(f"Invalid index parameter value for {k}: {v}") from e
|
|
1241
|
+
if ivalue <= 0:
|
|
1242
|
+
continue
|
|
1243
|
+
sanitized[key] = ivalue
|
|
1244
|
+
|
|
1245
|
+
return kind, sanitized
|
|
1182
1246
|
|
|
1183
1247
|
|
|
1184
1248
|
def _namespace_to_text(
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "langgraph-checkpoint-postgres"
|
|
7
|
-
version = "3.0.
|
|
7
|
+
version = "3.0.2"
|
|
8
8
|
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
|
9
9
|
authors = []
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -19,7 +19,10 @@ dependencies = [
|
|
|
19
19
|
]
|
|
20
20
|
|
|
21
21
|
[project.urls]
|
|
22
|
-
|
|
22
|
+
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres"
|
|
23
|
+
Twitter = "https://x.com/LangChainAI"
|
|
24
|
+
Slack = "https://www.langchain.com/join-community"
|
|
25
|
+
Reddit = "https://www.reddit.com/r/LangChain/"
|
|
23
26
|
|
|
24
27
|
[dependency-groups]
|
|
25
28
|
test = [
|