langgraph-checkpoint-postgres 2.0.6__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.
Files changed (13) hide show
  1. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.7}/PKG-INFO +1 -1
  2. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.7}/langgraph/checkpoint/postgres/__init__.py +8 -10
  3. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.7}/langgraph/checkpoint/postgres/aio.py +8 -11
  4. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.7}/langgraph/store/postgres/base.py +98 -15
  5. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.7}/pyproject.toml +1 -1
  6. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.7}/README.md +0 -0
  7. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.7}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
  8. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.7}/langgraph/checkpoint/postgres/_internal.py +0 -0
  9. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.7}/langgraph/checkpoint/postgres/base.py +0 -0
  10. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.7}/langgraph/checkpoint/postgres/py.typed +0 -0
  11. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.7}/langgraph/store/postgres/__init__.py +0 -0
  12. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.7}/langgraph/store/postgres/aio.py +0 -0
  13. {langgraph_checkpoint_postgres-2.0.6 → 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.6
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
@@ -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
- try:
80
- row = cur.execute(
81
- "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
82
- ).fetchone()
83
- if row is None:
84
- version = -1
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
- try:
85
- results = await cur.execute(
86
- "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
87
- )
88
- row = await results.fetchone()
89
- if row is None:
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
- # TODO: Add an HNSW or IVFFlat index depending on config
108
- # First must improve the search query when filtering by
109
- # namespace
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 score
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
- WHERE s.prefix LIKE %s {filter_str}
360
- ORDER BY {score_operator} DESC
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
- f"{_namespace_to_text(op.namespace_prefix)}%",
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
- return "1 - (sv.embedding <-> %s::%s)"
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
- return "-(sv.embedding <#> %s::%s)"
943
- else: # cosine
944
- return "1 - (sv.embedding <=> %s::%s)"
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.6"
3
+ version = "2.0.7"
4
4
  description = "Library with a Postgres implementation of LangGraph checkpoint saver."
5
5
  authors = []
6
6
  license = "MIT"