langgraph-checkpoint-postgres 3.0.1__tar.gz → 3.0.3__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.3/.gitignore +102 -0
- langgraph_checkpoint_postgres-3.0.3/LICENSE +21 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/PKG-INFO +7 -3
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/checkpoint/postgres/__init__.py +6 -4
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/checkpoint/postgres/aio.py +8 -4
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/checkpoint/postgres/shallow.py +14 -8
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/store/postgres/aio.py +21 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/store/postgres/base.py +88 -21
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/pyproject.toml +6 -3
- langgraph_checkpoint_postgres-3.0.3/uv.lock +1346 -0
- langgraph_checkpoint_postgres-3.0.1/.gitignore +0 -184
- langgraph_checkpoint_postgres-3.0.1/uv.lock +0 -1198
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/Makefile +0 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/README.md +0 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/checkpoint/postgres/_internal.py +0 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/checkpoint/postgres/base.py +0 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/checkpoint/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/store/postgres/__init__.py +0 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/langgraph/store/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/tests/__init__.py +0 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/tests/compose-postgres.yml +0 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/tests/conftest.py +0 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/tests/embed_test_utils.py +0 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/tests/test_async.py +0 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/tests/test_async_store.py +0 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/tests/test_store.py +0 -0
- {langgraph_checkpoint_postgres-3.0.1 → langgraph_checkpoint_postgres-3.0.3}/tests/test_sync.py +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
.vs/
|
|
2
|
+
.vscode/
|
|
3
|
+
.idea/
|
|
4
|
+
# Byte-compiled / optimized / DLL files
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
*$py.class
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
pip-wheel-metadata/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.tox/
|
|
43
|
+
.nox/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
*.py,cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
|
|
54
|
+
# Django stuff:
|
|
55
|
+
*.log
|
|
56
|
+
local_settings.py
|
|
57
|
+
db.sqlite3
|
|
58
|
+
db.sqlite3-journal
|
|
59
|
+
|
|
60
|
+
# PyBuilder
|
|
61
|
+
target/
|
|
62
|
+
|
|
63
|
+
# Jupyter Notebook
|
|
64
|
+
.ipynb_checkpoints
|
|
65
|
+
notebooks/
|
|
66
|
+
|
|
67
|
+
# IPython
|
|
68
|
+
profile_default/
|
|
69
|
+
ipython_config.py
|
|
70
|
+
|
|
71
|
+
# pyenv
|
|
72
|
+
.python-version
|
|
73
|
+
|
|
74
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
75
|
+
__pypackages__/
|
|
76
|
+
|
|
77
|
+
# Environments
|
|
78
|
+
.env
|
|
79
|
+
.envrc
|
|
80
|
+
.venv
|
|
81
|
+
.venvs
|
|
82
|
+
env/
|
|
83
|
+
venv/
|
|
84
|
+
ENV/
|
|
85
|
+
env.bak/
|
|
86
|
+
venv.bak/
|
|
87
|
+
|
|
88
|
+
# mypy
|
|
89
|
+
.mypy_cache/
|
|
90
|
+
.dmypy.json
|
|
91
|
+
dmypy.json
|
|
92
|
+
|
|
93
|
+
# Pyre type checker
|
|
94
|
+
.pyre/
|
|
95
|
+
|
|
96
|
+
# macOS display setting files
|
|
97
|
+
.DS_Store
|
|
98
|
+
|
|
99
|
+
.vercel
|
|
100
|
+
.turbo
|
|
101
|
+
.editorconfig
|
|
102
|
+
.scratch
|
|
@@ -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,11 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langgraph-checkpoint-postgres
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.3
|
|
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
|
-
Requires-Dist: langgraph-checkpoint<
|
|
12
|
+
Requires-Dist: langgraph-checkpoint<5.0.0,>=2.1.2
|
|
9
13
|
Requires-Dist: orjson>=3.10.1
|
|
10
14
|
Requires-Dist: psycopg-pool>=3.2.0
|
|
11
15
|
Requires-Dist: psycopg>=3.2.0
|
|
@@ -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
|
|
@@ -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(
|
|
@@ -146,6 +151,7 @@ class PoolConfig(TypedDict, total=False):
|
|
|
146
151
|
"""Connection pool settings for PostgreSQL connections.
|
|
147
152
|
|
|
148
153
|
Controls connection lifecycle and resource utilization:
|
|
154
|
+
|
|
149
155
|
- Small pools (1-5) suit low-concurrency workloads
|
|
150
156
|
- Larger pools handle concurrent requests but consume more resources
|
|
151
157
|
- Setting max_size prevents resource exhaustion under load
|
|
@@ -161,6 +167,7 @@ class PoolConfig(TypedDict, total=False):
|
|
|
161
167
|
"""Additional connection arguments passed to each connection in the pool.
|
|
162
168
|
|
|
163
169
|
Default kwargs set automatically:
|
|
170
|
+
|
|
164
171
|
- autocommit: True
|
|
165
172
|
- prepare_threshold: 0
|
|
166
173
|
- row_factory: dict_row
|
|
@@ -322,31 +329,36 @@ class BasePostgresStore(Generic[C]):
|
|
|
322
329
|
embedding_request: tuple[str, Sequence[tuple[str, str, str, str]]] | None = None
|
|
323
330
|
if inserts:
|
|
324
331
|
values = []
|
|
325
|
-
insertion_params = []
|
|
332
|
+
insertion_params: list[Any] = []
|
|
326
333
|
vector_values = []
|
|
327
334
|
embedding_request_params = []
|
|
328
335
|
# Handle TTL expiration
|
|
329
336
|
|
|
330
337
|
# First handle main store insertions
|
|
331
338
|
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
339
|
insertion_params.extend(
|
|
343
|
-
|
|
340
|
+
(
|
|
344
341
|
_namespace_to_text(op.namespace),
|
|
345
342
|
op.key,
|
|
346
343
|
Jsonb(cast(dict, op.value)),
|
|
347
|
-
|
|
348
|
-
]
|
|
344
|
+
)
|
|
349
345
|
)
|
|
346
|
+
if op.ttl is not None:
|
|
347
|
+
values.append(
|
|
348
|
+
"(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NOW() + %s::interval, %s)"
|
|
349
|
+
)
|
|
350
|
+
ttl_minutes = float(op.ttl)
|
|
351
|
+
insertion_params.extend(
|
|
352
|
+
(
|
|
353
|
+
f"{ttl_minutes * 60} seconds",
|
|
354
|
+
ttl_minutes,
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
else:
|
|
358
|
+
values.append(
|
|
359
|
+
"(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, %s)"
|
|
360
|
+
)
|
|
361
|
+
insertion_params.append(None)
|
|
350
362
|
|
|
351
363
|
# Then handle embeddings if configured
|
|
352
364
|
if self.index_config:
|
|
@@ -460,6 +472,10 @@ class BasePostgresStore(Generic[C]):
|
|
|
460
472
|
cast(dict, self.index_config)["dims"],
|
|
461
473
|
)
|
|
462
474
|
else:
|
|
475
|
+
if vector_type not in ("vector", "halfvec"):
|
|
476
|
+
raise ValueError(
|
|
477
|
+
f"Invalid vector_type for pgvector: {vector_type}"
|
|
478
|
+
)
|
|
463
479
|
score_operator = score_operator % ("%s", vector_type)
|
|
464
480
|
|
|
465
481
|
vectors_per_doc_estimate = cast(dict, self.index_config)[
|
|
@@ -642,7 +658,8 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|
|
642
658
|
item = store.get(("users", "123"), "prefs")
|
|
643
659
|
```
|
|
644
660
|
|
|
645
|
-
Or using the convenient from_conn_string helper:
|
|
661
|
+
Or using the convenient `from_conn_string` helper:
|
|
662
|
+
|
|
646
663
|
```python
|
|
647
664
|
from langgraph.store.postgres import PostgresStore
|
|
648
665
|
|
|
@@ -1117,6 +1134,27 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|
|
1117
1134
|
k: v(self) if v is not None and callable(v) else v
|
|
1118
1135
|
for k, v in migration.params.items()
|
|
1119
1136
|
}
|
|
1137
|
+
if "dims" in params:
|
|
1138
|
+
try:
|
|
1139
|
+
params["dims"] = int(params["dims"])
|
|
1140
|
+
except Exception as e:
|
|
1141
|
+
raise ValueError(
|
|
1142
|
+
f"Invalid dims for vector index: {params['dims']}"
|
|
1143
|
+
) from e
|
|
1144
|
+
if "vector_type" in params:
|
|
1145
|
+
vt = str(params["vector_type"])
|
|
1146
|
+
if vt not in ("vector", "halfvec"):
|
|
1147
|
+
raise ValueError(
|
|
1148
|
+
f"Invalid vector_type for pgvector: {vt}"
|
|
1149
|
+
)
|
|
1150
|
+
params["vector_type"] = vt
|
|
1151
|
+
if "index_type" in params:
|
|
1152
|
+
it = str(params["index_type"])
|
|
1153
|
+
if it not in ("hnsw", "ivfflat"):
|
|
1154
|
+
raise ValueError(
|
|
1155
|
+
f"Invalid index_type for pgvector: {it}"
|
|
1156
|
+
)
|
|
1157
|
+
params["index_type"] = it
|
|
1120
1158
|
sql = sql % params
|
|
1121
1159
|
cur.execute(sql)
|
|
1122
1160
|
cur.execute("INSERT INTO vector_migrations (v) VALUES (%s)", (v,))
|
|
@@ -1170,15 +1208,44 @@ def _get_vector_type_ops(store: BasePostgresStore) -> str:
|
|
|
1170
1208
|
|
|
1171
1209
|
|
|
1172
1210
|
def _get_index_params(store: Any) -> tuple[str, dict[str, Any]]:
|
|
1173
|
-
"""Get
|
|
1211
|
+
"""Get a sanitized index type and configuration based on config.
|
|
1212
|
+
|
|
1213
|
+
Only allow known-safe kinds and integer parameters to avoid SQL injection
|
|
1214
|
+
when constructing DDL strings for index creation.
|
|
1215
|
+
"""
|
|
1174
1216
|
if not store.index_config:
|
|
1175
1217
|
return "hnsw", {}
|
|
1176
1218
|
|
|
1177
1219
|
config = cast(PostgresIndexConfig, store.index_config)
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1220
|
+
raw = config.get("ann_index_config", _DEFAULT_ANN_CONFIG).copy()
|
|
1221
|
+
|
|
1222
|
+
kind = str(raw.pop("kind", "hnsw"))
|
|
1223
|
+
if kind not in ("hnsw", "ivfflat", "flat"):
|
|
1224
|
+
raise ValueError(
|
|
1225
|
+
f"Invalid index kind for pgvector: {kind}. Expected 'hnsw', 'ivfflat', or 'flat'."
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
raw.pop("vector_type", None)
|
|
1229
|
+
|
|
1230
|
+
if kind == "hnsw":
|
|
1231
|
+
allowed_keys = {"m", "ef_construction"}
|
|
1232
|
+
else: # ivfflat/flat
|
|
1233
|
+
allowed_keys = {"lists", "nlist"}
|
|
1234
|
+
|
|
1235
|
+
sanitized: dict[str, int] = {}
|
|
1236
|
+
for k, v in list(raw.items()):
|
|
1237
|
+
if k not in allowed_keys:
|
|
1238
|
+
continue
|
|
1239
|
+
key = "lists" if k == "nlist" else k
|
|
1240
|
+
try:
|
|
1241
|
+
ivalue = int(v) # type: ignore[call-overload]
|
|
1242
|
+
except Exception as e:
|
|
1243
|
+
raise ValueError(f"Invalid index parameter value for {k}: {v}") from e
|
|
1244
|
+
if ivalue <= 0:
|
|
1245
|
+
continue
|
|
1246
|
+
sanitized[key] = ivalue
|
|
1247
|
+
|
|
1248
|
+
return kind, sanitized
|
|
1182
1249
|
|
|
1183
1250
|
|
|
1184
1251
|
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.3"
|
|
8
8
|
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
|
9
9
|
authors = []
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -12,14 +12,17 @@ readme = "README.md"
|
|
|
12
12
|
license = "MIT"
|
|
13
13
|
license-files = ['LICENSE']
|
|
14
14
|
dependencies = [
|
|
15
|
-
"langgraph-checkpoint>=2.1.2,<
|
|
15
|
+
"langgraph-checkpoint>=2.1.2,<5.0.0",
|
|
16
16
|
"orjson>=3.10.1",
|
|
17
17
|
"psycopg>=3.2.0",
|
|
18
18
|
"psycopg-pool>=3.2.0",
|
|
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 = [
|