langgraph-checkpoint-postgres 2.0.5__tar.gz → 2.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-2.0.5 → langgraph_checkpoint_postgres-2.0.7}/PKG-INFO +3 -3
- {langgraph_checkpoint_postgres-2.0.5 → langgraph_checkpoint_postgres-2.0.7}/langgraph/checkpoint/postgres/__init__.py +8 -10
- {langgraph_checkpoint_postgres-2.0.5 → langgraph_checkpoint_postgres-2.0.7}/langgraph/checkpoint/postgres/aio.py +8 -11
- {langgraph_checkpoint_postgres-2.0.5 → langgraph_checkpoint_postgres-2.0.7}/langgraph/store/postgres/base.py +98 -15
- {langgraph_checkpoint_postgres-2.0.5 → langgraph_checkpoint_postgres-2.0.7}/pyproject.toml +3 -3
- {langgraph_checkpoint_postgres-2.0.5 → langgraph_checkpoint_postgres-2.0.7}/README.md +0 -0
- {langgraph_checkpoint_postgres-2.0.5 → langgraph_checkpoint_postgres-2.0.7}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
- {langgraph_checkpoint_postgres-2.0.5 → langgraph_checkpoint_postgres-2.0.7}/langgraph/checkpoint/postgres/_internal.py +0 -0
- {langgraph_checkpoint_postgres-2.0.5 → langgraph_checkpoint_postgres-2.0.7}/langgraph/checkpoint/postgres/base.py +0 -0
- {langgraph_checkpoint_postgres-2.0.5 → langgraph_checkpoint_postgres-2.0.7}/langgraph/checkpoint/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-2.0.5 → langgraph_checkpoint_postgres-2.0.7}/langgraph/store/postgres/__init__.py +0 -0
- {langgraph_checkpoint_postgres-2.0.5 → langgraph_checkpoint_postgres-2.0.7}/langgraph/store/postgres/aio.py +0 -0
- {langgraph_checkpoint_postgres-2.0.5 → langgraph_checkpoint_postgres-2.0.7}/langgraph/store/postgres/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: langgraph-checkpoint-postgres
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.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
|
|
@@ -13,8 +13,8 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Requires-Dist: langgraph-checkpoint (>=2.0.7,<3.0.0)
|
|
15
15
|
Requires-Dist: orjson (>=3.10.1)
|
|
16
|
-
Requires-Dist: psycopg (>=3.
|
|
17
|
-
Requires-Dist: psycopg-pool (>=3.
|
|
16
|
+
Requires-Dist: psycopg (>=3.2.0,<4.0.0)
|
|
17
|
+
Requires-Dist: psycopg-pool (>=3.2.0,<4.0.0)
|
|
18
18
|
Project-URL: Repository, https://www.github.com/langchain-ai/langgraph
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
|
|
@@ -5,7 +5,6 @@ from typing import Any, Optional
|
|
|
5
5
|
|
|
6
6
|
from langchain_core.runnables import RunnableConfig
|
|
7
7
|
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
|
8
|
-
from psycopg.errors import UndefinedTable
|
|
9
8
|
from psycopg.rows import DictRow, dict_row
|
|
10
9
|
from psycopg.types.json import Jsonb
|
|
11
10
|
from psycopg_pool import ConnectionPool
|
|
@@ -76,16 +75,15 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
76
75
|
the first time checkpointer is used.
|
|
77
76
|
"""
|
|
78
77
|
with self._cursor() as cur:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
else:
|
|
86
|
-
version = row["v"]
|
|
87
|
-
except UndefinedTable:
|
|
78
|
+
cur.execute(self.MIGRATIONS[0])
|
|
79
|
+
results = cur.execute(
|
|
80
|
+
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
|
|
81
|
+
)
|
|
82
|
+
row = results.fetchone()
|
|
83
|
+
if row is None:
|
|
88
84
|
version = -1
|
|
85
|
+
else:
|
|
86
|
+
version = row["v"]
|
|
89
87
|
for v, migration in zip(
|
|
90
88
|
range(version + 1, len(self.MIGRATIONS)),
|
|
91
89
|
self.MIGRATIONS[version + 1 :],
|
|
@@ -5,7 +5,6 @@ from typing import Any, Optional
|
|
|
5
5
|
|
|
6
6
|
from langchain_core.runnables import RunnableConfig
|
|
7
7
|
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
|
8
|
-
from psycopg.errors import UndefinedTable
|
|
9
8
|
from psycopg.rows import DictRow, dict_row
|
|
10
9
|
from psycopg.types.json import Jsonb
|
|
11
10
|
from psycopg_pool import AsyncConnectionPool
|
|
@@ -81,17 +80,15 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
81
80
|
the first time checkpointer is used.
|
|
82
81
|
"""
|
|
83
82
|
async with self._cursor() as cur:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
version = -1
|
|
91
|
-
else:
|
|
92
|
-
version = row["v"]
|
|
93
|
-
except UndefinedTable:
|
|
83
|
+
await cur.execute(self.MIGRATIONS[0])
|
|
84
|
+
results = await cur.execute(
|
|
85
|
+
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
|
|
86
|
+
)
|
|
87
|
+
row = await results.fetchone()
|
|
88
|
+
if row is None:
|
|
94
89
|
version = -1
|
|
90
|
+
else:
|
|
91
|
+
version = row["v"]
|
|
95
92
|
for v, migration in zip(
|
|
96
93
|
range(version + 1, len(self.MIGRATIONS)),
|
|
97
94
|
self.MIGRATIONS[version + 1 :],
|
|
@@ -56,6 +56,7 @@ class Migration(NamedTuple):
|
|
|
56
56
|
|
|
57
57
|
sql: str
|
|
58
58
|
params: Optional[dict[str, Any]] = None
|
|
59
|
+
condition: Optional[Callable[["BasePostgresStore"], bool]] = None
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
MIGRATIONS: Sequence[str] = [
|
|
@@ -104,11 +105,29 @@ CREATE TABLE IF NOT EXISTS store_vectors (
|
|
|
104
105
|
),
|
|
105
106
|
},
|
|
106
107
|
),
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
Migration(
|
|
109
|
+
"""
|
|
110
|
+
CREATE INDEX IF NOT EXISTS store_vectors_embedding_idx ON store_vectors
|
|
111
|
+
USING %(index_type)s (embedding %(ops)s)%(index_params)s;
|
|
112
|
+
""",
|
|
113
|
+
condition=lambda store: bool(
|
|
114
|
+
store.index_config and _get_index_params(store)[0] != "flat"
|
|
115
|
+
),
|
|
116
|
+
params={
|
|
117
|
+
"index_type": lambda store: _get_index_params(store)[0],
|
|
118
|
+
"ops": lambda store: _get_vector_type_ops(store),
|
|
119
|
+
"index_params": lambda store: (
|
|
120
|
+
" WITH ("
|
|
121
|
+
+ ", ".join(f"{k}={v}" for k, v in _get_index_params(store)[1].items())
|
|
122
|
+
+ ")"
|
|
123
|
+
if _get_index_params(store)[1]
|
|
124
|
+
else ""
|
|
125
|
+
),
|
|
126
|
+
},
|
|
127
|
+
),
|
|
110
128
|
]
|
|
111
129
|
|
|
130
|
+
|
|
112
131
|
C = TypeVar("C", bound=Union[_pg_internal.Conn, _ainternal.Conn])
|
|
113
132
|
|
|
114
133
|
|
|
@@ -140,6 +159,8 @@ class PoolConfig(TypedDict, total=False):
|
|
|
140
159
|
class ANNIndexConfig(TypedDict, total=False):
|
|
141
160
|
"""Configuration for vector index in PostgreSQL store."""
|
|
142
161
|
|
|
162
|
+
kind: Literal["hnsw", "ivfflat", "flat"]
|
|
163
|
+
"""Type of index to use: 'hnsw' for Hierarchical Navigable Small World, or 'ivfflat' for Inverted File Flat."""
|
|
143
164
|
vector_type: Literal["vector", "halfvec"]
|
|
144
165
|
"""Type of vector storage to use.
|
|
145
166
|
Options:
|
|
@@ -148,6 +169,35 @@ class ANNIndexConfig(TypedDict, total=False):
|
|
|
148
169
|
"""
|
|
149
170
|
|
|
150
171
|
|
|
172
|
+
class HNSWConfig(ANNIndexConfig, total=False):
|
|
173
|
+
"""Configuration for HNSW (Hierarchical Navigable Small World) index."""
|
|
174
|
+
|
|
175
|
+
kind: Literal["hnsw"] # type: ignore[misc]
|
|
176
|
+
m: int
|
|
177
|
+
"""Maximum number of connections per layer. Default is 16."""
|
|
178
|
+
ef_construction: int
|
|
179
|
+
"""Size of dynamic candidate list for index construction. Default is 64."""
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class IVFFlatConfig(ANNIndexConfig, total=False):
|
|
183
|
+
"""IVFFlat index divides vectors into lists, and then searches a subset of those lists that are closest to the query vector. It has faster build times and uses less memory than HNSW, but has lower query performance (in terms of speed-recall tradeoff).
|
|
184
|
+
|
|
185
|
+
Three keys to achieving good recall are:
|
|
186
|
+
1. Create the index after the table has some data
|
|
187
|
+
2. Choose an appropriate number of lists - a good place to start is rows / 1000 for up to 1M rows and sqrt(rows) for over 1M rows
|
|
188
|
+
3. When querying, specify an appropriate number of probes (higher is better for recall, lower is better for speed) - a good place to start is sqrt(lists)
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
kind: Literal["ivfflat"] # type: ignore[misc]
|
|
192
|
+
nlist: int
|
|
193
|
+
"""Number of inverted lists (clusters) for IVF index.
|
|
194
|
+
|
|
195
|
+
Determines the number of clusters used in the index structure.
|
|
196
|
+
Higher values can improve search speed but increase index size and build time.
|
|
197
|
+
Typically set to the square root of the number of vectors in the index.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
|
|
151
201
|
class PostgresIndexConfig(IndexConfig, total=False):
|
|
152
202
|
"""Configuration for vector embeddings in PostgreSQL store with pgvector-specific options.
|
|
153
203
|
|
|
@@ -321,7 +371,7 @@ class BasePostgresStore(Generic[C]):
|
|
|
321
371
|
if op.query and self.index_config:
|
|
322
372
|
embedding_requests.append((idx, op.query))
|
|
323
373
|
|
|
324
|
-
score_operator = _get_distance_operator(self)
|
|
374
|
+
score_operator, post_operator = _get_distance_operator(self)
|
|
325
375
|
vector_type = (
|
|
326
376
|
cast(PostgresIndexConfig, self.index_config)
|
|
327
377
|
.get("ann_index_config", {})
|
|
@@ -351,18 +401,28 @@ class BasePostgresStore(Generic[C]):
|
|
|
351
401
|
if not filter_conditions
|
|
352
402
|
else " AND " + " AND ".join(filter_conditions)
|
|
353
403
|
)
|
|
404
|
+
if op.namespace_prefix:
|
|
405
|
+
prefix_filter_str = f"WHERE s.prefix LIKE %s {filter_str} "
|
|
406
|
+
ns_args: Sequence = (f"{_namespace_to_text(op.namespace_prefix)}%",)
|
|
407
|
+
else:
|
|
408
|
+
ns_args = ()
|
|
409
|
+
if filter_str:
|
|
410
|
+
prefix_filter_str = f"WHERE {filter_str} "
|
|
411
|
+
else:
|
|
412
|
+
prefix_filter_str = ""
|
|
413
|
+
|
|
354
414
|
base_query = f"""
|
|
355
415
|
WITH scored AS (
|
|
356
|
-
SELECT s.prefix, s.key, s.value, s.created_at, s.updated_at, {score_operator} AS
|
|
416
|
+
SELECT s.prefix, s.key, s.value, s.created_at, s.updated_at, {score_operator} AS neg_score
|
|
357
417
|
FROM store s
|
|
358
418
|
JOIN store_vectors sv ON s.prefix = sv.prefix AND s.key = sv.key
|
|
359
|
-
|
|
360
|
-
ORDER BY {score_operator}
|
|
419
|
+
{prefix_filter_str}
|
|
420
|
+
ORDER BY {score_operator} ASC
|
|
361
421
|
LIMIT %s
|
|
362
422
|
)
|
|
363
423
|
SELECT * FROM (
|
|
364
424
|
SELECT DISTINCT ON (prefix, key)
|
|
365
|
-
prefix, key, value, created_at, updated_at, score
|
|
425
|
+
prefix, key, value, created_at, updated_at, {post_operator} as score
|
|
366
426
|
FROM scored
|
|
367
427
|
ORDER BY prefix, key, score DESC
|
|
368
428
|
) AS unique_docs
|
|
@@ -372,7 +432,7 @@ class BasePostgresStore(Generic[C]):
|
|
|
372
432
|
"""
|
|
373
433
|
params = [
|
|
374
434
|
_PLACEHOLDER, # Vector placeholder
|
|
375
|
-
|
|
435
|
+
*ns_args,
|
|
376
436
|
*filter_params,
|
|
377
437
|
_PLACEHOLDER,
|
|
378
438
|
expanded_limit,
|
|
@@ -702,7 +762,6 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|
|
702
762
|
_paramslist[i] = embedding
|
|
703
763
|
|
|
704
764
|
for (idx, _), (query, params) in zip(search_ops, queries):
|
|
705
|
-
# Execute the actual query
|
|
706
765
|
cur.execute(query, params)
|
|
707
766
|
rows = cast(list[Row], cur.fetchall())
|
|
708
767
|
results[idx] = [
|
|
@@ -765,6 +824,8 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|
|
765
824
|
for v, migration in enumerate(
|
|
766
825
|
self.VECTOR_MIGRATIONS[version + 1 :], start=version + 1
|
|
767
826
|
):
|
|
827
|
+
if migration.condition and not migration.condition(self):
|
|
828
|
+
continue
|
|
768
829
|
sql = migration.sql
|
|
769
830
|
if migration.params:
|
|
770
831
|
params = {
|
|
@@ -823,6 +884,18 @@ def _get_vector_type_ops(store: BasePostgresStore) -> str:
|
|
|
823
884
|
return f"{type_prefix}_{distance_suffix}"
|
|
824
885
|
|
|
825
886
|
|
|
887
|
+
def _get_index_params(store: Any) -> tuple[str, dict[str, Any]]:
|
|
888
|
+
"""Get the index type and configuration based on config."""
|
|
889
|
+
if not store.index_config:
|
|
890
|
+
return "hnsw", {}
|
|
891
|
+
|
|
892
|
+
config = cast(PostgresIndexConfig, store.index_config)
|
|
893
|
+
index_config = config.get("ann_index_config", _DEFAULT_ANN_CONFIG).copy()
|
|
894
|
+
kind = index_config.pop("kind", "hnsw")
|
|
895
|
+
index_config.pop("vector_type", None)
|
|
896
|
+
return kind, index_config
|
|
897
|
+
|
|
898
|
+
|
|
826
899
|
def _namespace_to_text(
|
|
827
900
|
namespace: tuple[str, ...], handle_wildcards: bool = False
|
|
828
901
|
) -> str:
|
|
@@ -915,7 +988,7 @@ def _decode_ns_bytes(namespace: Union[str, bytes, list]) -> tuple[str, ...]:
|
|
|
915
988
|
return tuple(namespace.split("."))
|
|
916
989
|
|
|
917
990
|
|
|
918
|
-
def _get_distance_operator(store: Any) -> str:
|
|
991
|
+
def _get_distance_operator(store: Any) -> tuple[str, str]:
|
|
919
992
|
"""Get the distance operator and score expression based on config."""
|
|
920
993
|
# Note: Today, we are not using ANN indices due to restrictions
|
|
921
994
|
# on PGVector's support for mixing vector and non-vector filters
|
|
@@ -936,12 +1009,22 @@ def _get_distance_operator(store: Any) -> str:
|
|
|
936
1009
|
config = cast(PostgresIndexConfig, store.index_config)
|
|
937
1010
|
distance_type = config.get("distance_type", "cosine")
|
|
938
1011
|
|
|
1012
|
+
# Return the operator and the score expression
|
|
1013
|
+
# The operator is used in the CTE and will be compatible with an ASCENDING ORDER
|
|
1014
|
+
# sort clause.
|
|
1015
|
+
# The score expression is used in the final query and will be compatible with
|
|
1016
|
+
# a DESCENDING ORDER sort clause and the user's expectations of what the similarity score
|
|
1017
|
+
# should be.
|
|
939
1018
|
if distance_type == "l2":
|
|
940
|
-
|
|
1019
|
+
# Final: "-(sv.embedding <-> %s::%s)"
|
|
1020
|
+
# We return the "l2 similarity" so that the sorting order is the same
|
|
1021
|
+
return "sv.embedding <-> %s::%s", "-scored.neg_score"
|
|
941
1022
|
elif distance_type == "inner_product":
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1023
|
+
# Final: "-(sv.embedding <#> %s::%s)"
|
|
1024
|
+
return "sv.embedding <#> %s::%s", "-(scored.neg_score)"
|
|
1025
|
+
else: # cosine similarity
|
|
1026
|
+
# Final: "1 - (sv.embedding <=> %s::%s)"
|
|
1027
|
+
return "sv.embedding <=> %s::%s", "1 - scored.neg_score"
|
|
945
1028
|
|
|
946
1029
|
|
|
947
1030
|
def _ensure_index_config(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "langgraph-checkpoint-postgres"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.7"
|
|
4
4
|
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
|
5
5
|
authors = []
|
|
6
6
|
license = "MIT"
|
|
@@ -12,8 +12,8 @@ packages = [{ include = "langgraph" }]
|
|
|
12
12
|
python = "^3.9.0,<4.0"
|
|
13
13
|
langgraph-checkpoint = "^2.0.7"
|
|
14
14
|
orjson = ">=3.10.1"
|
|
15
|
-
psycopg = "^3.
|
|
16
|
-
psycopg-pool = "^3.
|
|
15
|
+
psycopg = "^3.2.0"
|
|
16
|
+
psycopg-pool = "^3.2.0"
|
|
17
17
|
|
|
18
18
|
[tool.poetry.group.dev.dependencies]
|
|
19
19
|
ruff = "^0.6.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|