langgraph-api 0.12.0.dev13__py3-none-any.whl → 0.12.0.dev15__py3-none-any.whl

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_api/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.12.0.dev13"
1
+ __version__ = "0.12.0.dev15"
langgraph_api/asyncio.py CHANGED
@@ -103,6 +103,8 @@ async def wait_if_not_done(coro: Coroutine[Any, Any, T], done: ValueEvent) -> T:
103
103
  await logger.ainfo("Done awaiting.", task=str(fut))
104
104
  if e.args and isinstance(e.args[0], Exception):
105
105
  raise e.args[0] from None
106
+ if isinstance(done._value, Exception):
107
+ raise done._value from None
106
108
  raise
107
109
  except ExceptionGroup as e:
108
110
  raise e.exceptions[0] from None
@@ -243,6 +243,8 @@ WEBHOOKS_ENABLED = HTTP_CONFIG and HTTP_CONFIG.get("disable_webhooks")
243
243
  STORE_CONFIG = env(
244
244
  "LANGGRAPH_STORE", cast=_parse.parse_schema(StoreConfig), default=None
245
245
  )
246
+ LANGGRAPH_STORE_BACKEND = env("LANGGRAPH_STORE_BACKEND", cast=str, default="python")
247
+ USE_GRPC_STORE = LANGGRAPH_STORE_BACKEND == "grpc"
246
248
 
247
249
 
248
250
  def _validate_mount_prefix(mount_prefix: str | None) -> str | None:
@@ -668,6 +670,7 @@ __all__ = [
668
670
  "LANGGRAPH_METRICS_ENDPOINT",
669
671
  "LANGGRAPH_METRICS_EXPORT_INTERVAL_MS",
670
672
  "LANGGRAPH_POSTGRES_EXTENSIONS",
673
+ "LANGGRAPH_STORE_BACKEND",
671
674
  "LANGSMITH_API_KEY",
672
675
  "LANGSMITH_AUTH_ENDPOINT",
673
676
  "LANGSMITH_AUTH_VERIFY_TENANT_ID",
@@ -715,6 +718,7 @@ __all__ = [
715
718
  "USES_STORE_TTL",
716
719
  "USES_THREAD_TTL",
717
720
  "USE_CUSTOM_CHECKPOINTER",
721
+ "USE_GRPC_STORE",
718
722
  "AuthConfig",
719
723
  "CheckpointerConfig",
720
724
  "CorsConfig",
@@ -39,6 +39,7 @@ GRPC_STATUS_TO_HTTP_STATUS = {
39
39
  StatusCode.INVALID_ARGUMENT: HTTPStatus.UNPROCESSABLE_ENTITY,
40
40
  StatusCode.PERMISSION_DENIED: HTTPStatus.FORBIDDEN,
41
41
  StatusCode.UNAUTHENTICATED: HTTPStatus.UNAUTHORIZED,
42
+ StatusCode.RESOURCE_EXHAUSTED: HTTPStatus.TOO_MANY_REQUESTS,
42
43
  }
43
44
 
44
45
 
@@ -187,19 +188,33 @@ def _handle_grpc_error(error: AioRpcError) -> None:
187
188
  Always return detail as a string here.
188
189
  """
189
190
  error_details = error.details()
191
+ retry_after_seconds = None
190
192
  if error_details is not None:
191
193
  try:
192
194
  details = orjson.loads(error_details)
193
- error_details = orjson.dumps(details.get("message", "")).decode()
195
+ retry_after_seconds = details.get("retry_after_seconds")
196
+ message = details.get("message", "")
197
+ # A string message is the detail verbatim; only re-encode structured
198
+ # (non-string) messages as JSON.
199
+ error_details = (
200
+ message if isinstance(message, str) else orjson.dumps(message).decode()
201
+ )
194
202
  except orjson.JSONDecodeError:
195
203
  # error details is not json, so just retun it as is
196
204
  pass
197
205
 
206
+ status_code = GRPC_STATUS_TO_HTTP_STATUS.get(
207
+ error.code(), HTTPStatus.INTERNAL_SERVER_ERROR
208
+ )
209
+ headers = None
210
+ if status_code == HTTPStatus.TOO_MANY_REQUESTS and retry_after_seconds:
211
+ # The Go core carries Retry-After as a structured integer (seconds).
212
+ headers = {"Retry-After": str(int(retry_after_seconds))}
213
+
198
214
  raise HTTPException(
199
- status_code=GRPC_STATUS_TO_HTTP_STATUS.get(
200
- error.code(), HTTPStatus.INTERNAL_SERVER_ERROR
201
- ),
215
+ status_code=status_code,
202
216
  detail=error_details,
217
+ headers=headers,
203
218
  )
204
219
 
205
220
 
langgraph_api/metadata.py CHANGED
@@ -2,9 +2,9 @@ import asyncio
2
2
  import os
3
3
  import uuid
4
4
  from datetime import UTC, datetime
5
+ from importlib import metadata as importlib_metadata
5
6
  from typing import Any
6
7
 
7
- import langgraph.version
8
8
  import orjson
9
9
  import structlog
10
10
 
@@ -58,6 +58,10 @@ else:
58
58
  HOST = "self-hosted"
59
59
  PLAN = "enterprise" if plus_features_enabled() else "developer"
60
60
  USER_API_URL = os.getenv("LANGGRAPH_API_URL", None)
61
+ DEPENDENCY_VERSION_DISTRIBUTIONS = {
62
+ "langgraph.python.version": "langgraph",
63
+ "starlette.version": "starlette",
64
+ }
61
65
 
62
66
  RUN_COUNTER = 0
63
67
  NODE_COUNTER = 0
@@ -80,6 +84,19 @@ if LANGSMITH_AUTH_ENDPOINT:
80
84
  )
81
85
 
82
86
 
87
+ def _dependency_version_metadata() -> dict[str, str]:
88
+ tags = {}
89
+ for tag, distribution in DEPENDENCY_VERSION_DISTRIBUTIONS.items():
90
+ try:
91
+ tags[tag] = importlib_metadata.version(distribution)
92
+ except importlib_metadata.PackageNotFoundError:
93
+ logger.debug(
94
+ "Dependency package not found for metadata", package=distribution
95
+ )
96
+ tags[tag] = ""
97
+ return tags
98
+
99
+
83
100
  def _lang_usage_metadata() -> tuple[dict[str, str], dict[str, int]]:
84
101
  js_graph_count = sum(1 for graph_id in GRAPHS if is_js_graph(graph_id))
85
102
  py_graph_count = len(GRAPHS) - js_graph_count
@@ -130,8 +147,6 @@ async def metadata_loop() -> None:
130
147
  "No license key or control plane API key set, skipping metadata loop"
131
148
  )
132
149
  return
133
- lg_version = langgraph.version.__version__
134
-
135
150
  if (
136
151
  LANGGRAPH_CLOUD_LICENSE_KEY
137
152
  and not LANGGRAPH_CLOUD_LICENSE_KEY.startswith("lcl_")
@@ -160,7 +175,7 @@ async def metadata_loop() -> None:
160
175
  base_tags = _ensure_strings(
161
176
  # Tag values must be strings.
162
177
  {
163
- "langgraph.python.version": lg_version,
178
+ **_dependency_version_metadata(),
164
179
  "langgraph_api.version": __version__ or "",
165
180
  "langgraph.platform.revision": REVISION or "",
166
181
  "langgraph.platform.variant": VARIANT or "",
langgraph_api/stream.py CHANGED
@@ -339,7 +339,26 @@ async def astream_state(
339
339
  ):
340
340
  yield event["name"], event["data"]
341
341
  # TODO support messages-tuple for js graphs
342
- if event["event"] == "on_chain_stream" and event["run_id"] == run_id:
342
+ # For JS (BaseRemotePregel) with subgraphs=True, subgraph events
343
+ # carry the subgraph's own run_id. Accept them if the chunk is a
344
+ # proper 3-tuple [ns, mode, chunk] — that's the sidecar's
345
+ # normalized format. Limit this bypass to remote graphs only;
346
+ # Python child events must fall through to the raw "events"
347
+ # fallback below when the client requests stream_mode="events".
348
+ _chunk_data = (
349
+ event["data"].get("chunk")
350
+ if event["event"] == "on_chain_stream"
351
+ else None
352
+ )
353
+ if event["event"] == "on_chain_stream" and (
354
+ event["run_id"] == run_id
355
+ or (
356
+ subgraphs
357
+ and is_remote_pregel
358
+ and isinstance(_chunk_data, (list, tuple))
359
+ and len(_chunk_data) == 3
360
+ )
361
+ ):
343
362
  if subgraphs:
344
363
  ns, mode, chunk = event["data"]["chunk"]
345
364
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-api
3
- Version: 0.12.0.dev13
3
+ Version: 0.12.0.dev15
4
4
  Author-email: Will Fu-Hinthorn <will@langchain.dev>, Josh Rogers <josh@langchain.dev>, Parker Rule <parker@langchain.dev>
5
5
  License: Elastic-2.0
6
6
  License-File: LICENSE
@@ -29,7 +29,7 @@ Requires-Dist: prometheus-client>=0.0.1
29
29
  Requires-Dist: protobuf<7.0.0,>=6.32.1
30
30
  Requires-Dist: pyjwt>=2.13.0
31
31
  Requires-Dist: sse-starlette<3.4.0,>=2.1.3
32
- Requires-Dist: starlette>=0.38.6
32
+ Requires-Dist: starlette>=1.3.1
33
33
  Requires-Dist: structlog<26,>=24.1.0
34
34
  Requires-Dist: tenacity>=8.0.0
35
35
  Requires-Dist: truststore>=0.1
@@ -1,7 +1,7 @@
1
- langgraph_api/__init__.py,sha256=hom8LDza4isfZKrYaGZxZ5cj1otdh2O5s3xZqMaK8zc,29
1
+ langgraph_api/__init__.py,sha256=3hHYNZlyWnoc9JVCLo-c7OAsrbQDPhSBvtrj8CKX1z4,29
2
2
  langgraph_api/_factory_utils.py,sha256=5JsiJbg_YocVSryN2jwoZTg03-eyymlWMK6sKCmXwz0,5756
3
3
  langgraph_api/asgi_transport.py,sha256=XApY3lIWBZTMbbsl8dDJzl0cLGirmAGE0SifqZUnXvs,11896
4
- langgraph_api/asyncio.py,sha256=c-YE-14N7_AP1GzifsbP14XnhLsmxT2P916KXruerpI,10573
4
+ langgraph_api/asyncio.py,sha256=smqyAoO9nIyPPQRw1IVSCh85fwoZP4PEo-5dDhxGruc,10676
5
5
  langgraph_api/cache.py,sha256=LuB3Te0UdXC8a-uEJoRHoe5XsgXulXI59f_Q6DCJNKc,13005
6
6
  langgraph_api/cli.py,sha256=ATtS9s9Cx7QNiGPJceKnMCko29A25ZA-xz39fdxmgfg,22389
7
7
  langgraph_api/command.py,sha256=d-k8h6H4ix1n7fSZ-Zb01NbSkEyqrD6cMKfDFXEIYEw,821
@@ -13,7 +13,7 @@ langgraph_api/http.py,sha256=7hPxKbj-xoAKcm7iucBpT5nM_hXOgGVCPbBsCD693Cw,6977
13
13
  langgraph_api/http_metrics.py,sha256=etxbZNmYxdb58DVLNkHP7S-N6njXPTiQh2OWKMaIZi8,5336
14
14
  langgraph_api/http_metrics_utils.py,sha256=sjxF7SYGTzY0Wz_G0dzatsYNnWr31S6ujej4JmBG2yo,866
15
15
  langgraph_api/logging.py,sha256=V1RCnqVLuMvJtrBiyMMLfaEdbS3k5A2M8Unhr4FUUdQ,6801
16
- langgraph_api/metadata.py,sha256=ih2et_R0prFsCzikQ4_L0j9up7t0rObAMVKyEk7ienI,9778
16
+ langgraph_api/metadata.py,sha256=hXkF55cWxxnjAJ0MB_PhU_4NfVzpLuwU4xwFDaAgi7I,10314
17
17
  langgraph_api/metrics_collector.py,sha256=gMLHL18rJyYl985AOmu9eH7W1ttdRdkPHzeyczjCOBw,8280
18
18
  langgraph_api/metrics_otlp.py,sha256=t9oJrxfxY2O5jY4JW2gONPKoBiBuklhzCrnZvn1qTxQ,28730
19
19
  langgraph_api/otel_context.py,sha256=DWFwW4Yu88QY4W2J0IRcURR450Th9J2DupvDDkSkMBA,7166
@@ -29,7 +29,7 @@ langgraph_api/server.py,sha256=1eAZPim0Pkgh5oGS4EvW-_7Zh_82iGOZtR1rpX08FoA,11216
29
29
  langgraph_api/sse.py,sha256=cChZ7raQUHp8p5BreE_5wMBR8lFO0n7746sV8_HQOrc,4822
30
30
  langgraph_api/state.py,sha256=A6jx2OpcVmiFNBafjZ2tX1x6s_AhBk5EW2drd-vaqIc,4396
31
31
  langgraph_api/store.py,sha256=i-jVfWzDEmPESVofh1tNWbo0HMLiLz_BSKKV4QjN-dE,5027
32
- langgraph_api/stream.py,sha256=MGWh1NvDGPqNgVXp-3wknqpam9g9nrqaMlKMXCWwx6M,32407
32
+ langgraph_api/stream.py,sha256=rJIGe8rEeS6QxQfsDKomUBRED6ZPIF-ALtx4D8Gw0JQ,33348
33
33
  langgraph_api/stream_v2.py,sha256=KCIsPf6gs_jrIdZ8D2VTEi5c5p5aRNpVNhMP4kzjp2g,10824
34
34
  langgraph_api/traceblock.py,sha256=GhgAtLhAQ8Z8LHtRQq3par-rfPpRBQp2Q7WBACJq1NI,677
35
35
  langgraph_api/validation.py,sha256=XyeKyt7jAICmIlT_b0J0mv2YbwIbNoe4m6zEmfk9gOA,14657
@@ -64,7 +64,7 @@ langgraph_api/auth/studio_user.py,sha256=gNCicIo6cYaHmFj2sEdsvDYkKW7NWfGXGS2tTAM
64
64
  langgraph_api/auth/langsmith/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
65
  langgraph_api/auth/langsmith/backend.py,sha256=Y6-VxD7zfV1jzGdjmQ66CgNa3SenLbo3d_375CcKZ9U,3770
66
66
  langgraph_api/auth/langsmith/client.py,sha256=79kwCVeHU64nsHsxWipfZhf44lM6vfs2nlfTxlJF6LU,4142
67
- langgraph_api/config/__init__.py,sha256=qj7HF1XmojpG6WXahlFxrMZiO8hUI6QlPNVT-32xfvs,25528
67
+ langgraph_api/config/__init__.py,sha256=NYJWKJ-PbfeI6k5KnF3W_HWLE8EOS0rJKaQQqKTCAus,25717
68
68
  langgraph_api/config/_parse.py,sha256=RpfZgFAPJlRMv13AzGr3kAYbIrqHcgjzO8IgeboTw4A,3922
69
69
  langgraph_api/config/schemas.py,sha256=cHzVvepthpD7DDeWE2ytkOHah-iDNH7xRx9dSWUatQI,20864
70
70
  langgraph_api/encryption/__init__.py,sha256=gaCZ00CocSbqSqrDn6XJHaSp2CZCnC8qnrD9G4fbzyI,363
@@ -86,7 +86,7 @@ langgraph_api/grpc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
86
86
  langgraph_api/grpc/client.py,sha256=ypSgZWclK7OCKLGW8gD_LZwwgsgQEahJqc8mBWF8Rug,15578
87
87
  langgraph_api/grpc/server.py,sha256=Fknr6BJbdSK38wF7B1vPDplK25KB_UfMB9Fp1er6DPU,8366
88
88
  langgraph_api/grpc/generated/core_api_pb2.pyi,sha256=0PEzVh8s6rqHq52ekhqPaMI_TOsP4_xuUUv9_XS7r1w,54758
89
- langgraph_api/grpc/ops/__init__.py,sha256=k2Ehs9LbMgYQp5rZ0QlxVA18OWJJd64UotVrB9GwGnM,18148
89
+ langgraph_api/grpc/ops/__init__.py,sha256=ACO-Cp7ygehNfOG1ucSH8iMw3hVXbLluxiqczG2PKTQ,18827
90
90
  langgraph_api/grpc/ops/assistants.py,sha256=00vY7Dd7BcXaVp_6UrKZIb5b4tQ8Lm5MxJcEpyVPfo0,13902
91
91
  langgraph_api/grpc/ops/cache.py,sha256=MoPAmSuOwbt2Cud1YZJm_YJeaDHPFy1IiV4m0PDImgg,1399
92
92
  langgraph_api/grpc/ops/crons.py,sha256=oGPW9qW-J4H9baX7Jsee1opsFnXfThHLHh_N47mDf9s,19596
@@ -165,14 +165,16 @@ openapi.json,sha256=0N-D4j0_3ruH6nMF4k5lnqmpV-aoB5Se-WWheCQuQQs,219552
165
165
  langgraph_grpc_common/__init__.py,sha256=sO7jFHekQxt6zZPhTPZAWLYLB_S8w7-VuJ7NyWFlFNo,97
166
166
  langgraph_grpc_common/checkpointer.py,sha256=u3ERFgqnUCpCYpOSxKR_6Jvo_tR4yDvq7qU-EL61bSg,14022
167
167
  langgraph_grpc_common/serde.py,sha256=pxIhgxtrXtwWVhAlLE2LAeSAuKk2ZHtnvAP3DRDT2Qw,6184
168
+ langgraph_grpc_common/store.py,sha256=BIHYT_BTw2ByFAIsomu9vwNVu1kZdUEOjkJChV7rjuA,3721
168
169
  langgraph_grpc_common/conversion/__init__.py,sha256=pLsOrRtmF9YkgmCPhcfym6lYOB7PCJTRItyo1YK_AT8,208
169
170
  langgraph_grpc_common/conversion/_compat.py,sha256=MIEo69I7tfaPskunPGwBCKheksLwxpw-z-4O3hvO9og,4010
170
171
  langgraph_grpc_common/conversion/checkpoint.py,sha256=Ty81cijafeal7blzzXbYFjWpu9iXVb_spFLBZ2FvmgI,12045
171
172
  langgraph_grpc_common/conversion/config.py,sha256=HjlHRX-_GpKa1m0RWVL1PEZQCvA3w_8wKVYrtaT06do,13015
172
173
  langgraph_grpc_common/conversion/durability.py,sha256=FWhqjzVN4JrrsAL9AwsJkvIorQGNRl14w97Va-WZRk0,991
174
+ langgraph_grpc_common/conversion/store.py,sha256=NGGZp4FiMzJXByIqRRy2qYDxlWeXBGkEN9k1aNTsVUA,6253
173
175
  langgraph_grpc_common/conversion/struct.py,sha256=yk4jpPdywzn8UosAohun21lU5D_IIglOY9rkBhiG-gA,2072
174
176
  langgraph_grpc_common/conversion/value.py,sha256=eZ9fDqKje9tKsO8A_6f0BPSI7lt94Vzuu3NNEEYLFBM,8343
175
- langgraph_grpc_common/proto/__init__.py,sha256=cFnJ18b62FnqKcZHz9wjKES0QetC_5twG47G1W0A2mM,1283
177
+ langgraph_grpc_common/proto/__init__.py,sha256=eBejrrULiDdasi3_Hga_AJW8ZPMkjKyyAeW9Ey640Ks,1375
176
178
  langgraph_grpc_common/proto/checkpointer_pb2.py,sha256=c-gaVwNHm7sUzThJDzumME2M_nP4B1RjEki4ZG7-kjM,7915
177
179
  langgraph_grpc_common/proto/checkpointer_pb2.pyi,sha256=rGUxQyNFifmu8OPy0S418sCWSBQY_CVckobPXtFfZfs,20763
178
180
  langgraph_grpc_common/proto/checkpointer_pb2_grpc.py,sha256=MLhZ7M4g9gjfXZqWenk93oO2uIZSybbFoaeU8bauFPA,20176
@@ -229,8 +231,12 @@ langgraph_grpc_common/proto/errors_pb2.py,sha256=JI6x-vBK1AE7DHZ5DQwN1mZWF6C4xTR
229
231
  langgraph_grpc_common/proto/errors_pb2.pyi,sha256=rd3-BYUH8V-aO66taL7OOblaLgdrDtf1Vcd38GUoVVM,2181
230
232
  langgraph_grpc_common/proto/errors_pb2_grpc.py,sha256=2-LwQ0OPGo-NtC0269q7Fw6GPBxnTLYWq3xP5Eq0_YA,886
231
233
  langgraph_grpc_common/proto/errors_pb2_grpc.pyi,sha256=uC9Wnq6uyg488QiONpJ0ba1s_iouQCOYsjd_FDd1XUM,495
232
- langgraph_api-0.12.0.dev13.dist-info/METADATA,sha256=JurA5etESRZZMt2ew_YYMC9wLFzGh53TVBzR6b5l0to,4632
233
- langgraph_api-0.12.0.dev13.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
234
- langgraph_api-0.12.0.dev13.dist-info/entry_points.txt,sha256=hGedv8n7cgi41PypMfinwS_HfCwA7xJIfS0jAp8htV8,78
235
- langgraph_api-0.12.0.dev13.dist-info/licenses/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
236
- langgraph_api-0.12.0.dev13.dist-info/RECORD,,
234
+ langgraph_grpc_common/proto/store_pb2.py,sha256=5px40nIUag8p0KlE6o5vpvgfenX71BDIr3VjFfFQP6E,3406
235
+ langgraph_grpc_common/proto/store_pb2.pyi,sha256=cZZGod9tBReUukPkjNzssEdz5OBoZ2pecXugbp-9Lmo,9755
236
+ langgraph_grpc_common/proto/store_pb2_grpc.py,sha256=wSD65QgFkYTkPTHcf7VhyI2FQ7UH_RgIf3vQW3VYlsM,3324
237
+ langgraph_grpc_common/proto/store_pb2_grpc.pyi,sha256=oS1h2cDoq2OjyM8xazcmc8LwRDC5oCAD0zto2QUmPQw,2027
238
+ langgraph_api-0.12.0.dev15.dist-info/METADATA,sha256=nc5DmJfxPOxGO2rAoxSIuNyd_s0yNxlCB-M8FZGlWL8,4631
239
+ langgraph_api-0.12.0.dev15.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
240
+ langgraph_api-0.12.0.dev15.dist-info/entry_points.txt,sha256=hGedv8n7cgi41PypMfinwS_HfCwA7xJIfS0jAp8htV8,78
241
+ langgraph_api-0.12.0.dev15.dist-info/licenses/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
242
+ langgraph_api-0.12.0.dev15.dist-info/RECORD,,
@@ -0,0 +1,208 @@
1
+ """Marshal langgraph.store.base GetOp/PutOp/Item/Result to and from store.proto."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from typing import TYPE_CHECKING, Any, cast
7
+
8
+ import orjson
9
+ from google.protobuf import empty_pb2
10
+ from langgraph.store.base import (
11
+ GetOp,
12
+ IndexConfig,
13
+ Item,
14
+ Op,
15
+ PutOp,
16
+ Result,
17
+ ensure_embeddings,
18
+ get_text_at_path,
19
+ tokenize_path,
20
+ )
21
+
22
+ from langgraph_grpc_common.proto import store_pb2
23
+
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Sequence
26
+
27
+ from langchain_core.embeddings import Embeddings
28
+
29
+
30
+ def _ensure_index_config(
31
+ index_config: IndexConfig,
32
+ ) -> tuple[Embeddings | None, IndexConfig]:
33
+ # Vendored from langgraph.store.postgres.base so grpc_common_py stays
34
+ # backend-agnostic (no langgraph-checkpoint-postgres / psycopg dependency).
35
+ index_config = index_config.copy()
36
+ tokenized: list[tuple[str, str | list[str]]] = []
37
+ tot = 0
38
+ fields = index_config.get("fields") or ["$"]
39
+ if isinstance(fields, str):
40
+ fields = [fields]
41
+ if not isinstance(fields, list):
42
+ raise ValueError(f"Text fields must be a list or a string. Got {fields}")
43
+ for p in fields:
44
+ if p == "$":
45
+ tokenized.append((p, "$"))
46
+ tot += 1
47
+ else:
48
+ toks = tokenize_path(p)
49
+ tokenized.append((p, toks))
50
+ tot += len(toks)
51
+ index_config["__tokenized_fields"] = tokenized # ty: ignore[invalid-key]
52
+ index_config["__estimated_num_vectors"] = tot # ty: ignore[invalid-key]
53
+ embeddings = ensure_embeddings(index_config.get("embed"))
54
+ return embeddings, index_config
55
+
56
+
57
+ def _ttl_to_proto_minutes(ttl: float | None) -> int:
58
+ if ttl is None:
59
+ return 0
60
+ if ttl != int(ttl):
61
+ raise ValueError(
62
+ f"gRPC store only supports integer-minute TTL; got fractional ttl={ttl}"
63
+ )
64
+ return int(ttl)
65
+
66
+
67
+ def op_to_proto(
68
+ op: Op,
69
+ *,
70
+ vectors: Sequence[store_pb2.VectorValue] | None = None,
71
+ ) -> store_pb2.Op:
72
+ if isinstance(op, GetOp):
73
+ return store_pb2.Op(
74
+ get=store_pb2.GetOp(
75
+ namespace=list(op.namespace),
76
+ key=str(op.key),
77
+ refreshTTL=op.refresh_ttl,
78
+ )
79
+ )
80
+ if isinstance(op, PutOp):
81
+ if op.value is None:
82
+ raise TypeError(
83
+ "PutOp with value=None (delete) is not supported by gRPC store"
84
+ )
85
+ put_kwargs: dict[str, Any] = {
86
+ "namespace": list(op.namespace),
87
+ "key": str(op.key),
88
+ "value_json": orjson.dumps(op.value),
89
+ "ttlMinutes": _ttl_to_proto_minutes(op.ttl),
90
+ }
91
+ if vectors is not None:
92
+ put_kwargs["vectors"] = list(vectors)
93
+ return store_pb2.Op(put=store_pb2.PutOp(**put_kwargs))
94
+ raise TypeError(f"unsupported op: {type(op).__name__}")
95
+
96
+
97
+ def op_from_proto(msg: store_pb2.Op) -> Op:
98
+ kind = msg.WhichOneof("op")
99
+ if kind == "get":
100
+ g = msg.get
101
+ return GetOp(
102
+ namespace=tuple(g.namespace),
103
+ key=g.key,
104
+ refresh_ttl=g.refreshTTL,
105
+ )
106
+ if kind == "put":
107
+ p = msg.put
108
+ ttl: float | None = None
109
+ if p.ttlMinutes:
110
+ ttl = float(p.ttlMinutes)
111
+ return PutOp(
112
+ namespace=tuple(p.namespace),
113
+ key=p.key,
114
+ value=orjson.loads(p.value_json),
115
+ ttl=ttl,
116
+ )
117
+ raise ValueError("empty Op message (no oneof set)")
118
+
119
+
120
+ def result_to_proto(result: Result, op: Op) -> store_pb2.OpResult:
121
+ if isinstance(op, GetOp):
122
+ if result is None:
123
+ return store_pb2.OpResult(none=empty_pb2.Empty())
124
+ if not isinstance(result, Item):
125
+ raise TypeError(
126
+ f"expected Item for GetOp result, got {type(result).__name__}"
127
+ )
128
+ return store_pb2.OpResult(item=_item_to_proto(result))
129
+ if isinstance(op, PutOp):
130
+ return store_pb2.OpResult(none=empty_pb2.Empty())
131
+ raise TypeError(f"unsupported op for result encoding: {type(op).__name__}")
132
+
133
+
134
+ def result_from_proto(msg: store_pb2.OpResult) -> Result:
135
+ kind = msg.WhichOneof("result")
136
+ if kind == "none":
137
+ return None
138
+ if kind == "item":
139
+ return _item_from_proto(msg.item)
140
+ raise ValueError("empty OpResult message (no oneof set)")
141
+
142
+
143
+ def _item_to_proto(item: Item) -> store_pb2.Item:
144
+ return store_pb2.Item(
145
+ namespace=list(item.namespace),
146
+ key=item.key,
147
+ value_json=orjson.dumps(item.value),
148
+ created_at=_format_dt(item.created_at),
149
+ updated_at=_format_dt(item.updated_at),
150
+ )
151
+
152
+
153
+ def _item_from_proto(msg: store_pb2.Item) -> Item:
154
+ return Item(
155
+ namespace=tuple(msg.namespace),
156
+ key=msg.key,
157
+ value=orjson.loads(msg.value_json),
158
+ created_at=datetime.fromisoformat(msg.created_at),
159
+ updated_at=datetime.fromisoformat(msg.updated_at),
160
+ )
161
+
162
+
163
+ def _format_dt(dt: datetime) -> str:
164
+ return dt.isoformat()
165
+
166
+
167
+ async def vectors_for_put_op(
168
+ op: PutOp,
169
+ index_config: IndexConfig,
170
+ embeddings: Embeddings,
171
+ ) -> list[store_pb2.VectorValue]:
172
+ if op.index is False or op.value is None:
173
+ return []
174
+
175
+ value = op.value
176
+ if op.index is None:
177
+ paths = cast(
178
+ "list[tuple[str, str | list[str]]]",
179
+ index_config["__tokenized_fields"], # ty: ignore[invalid-key]
180
+ )
181
+ else:
182
+ paths = [(ix, tokenize_path(ix)) for ix in op.index]
183
+
184
+ texts: list[str] = []
185
+ pathnames: list[str] = []
186
+ for path, tokenized_path in paths:
187
+ extracted = get_text_at_path(value, tokenized_path)
188
+ for i, text in enumerate(extracted):
189
+ pathname = f"{path}.{i}" if len(extracted) > 1 else path
190
+ pathnames.append(pathname)
191
+ texts.append(text)
192
+
193
+ if not texts:
194
+ return []
195
+
196
+ vectors = await embeddings.aembed_documents(texts)
197
+ return [
198
+ store_pb2.VectorValue(valuePath=pathname, embedding=vector)
199
+ for pathname, vector in zip(pathnames, vectors, strict=False)
200
+ ]
201
+
202
+
203
+ def normalize_index_config(
204
+ index: IndexConfig | None,
205
+ ) -> tuple[Embeddings | None, IndexConfig | None]:
206
+ if not index:
207
+ return None, None
208
+ return _ensure_index_config(index)
@@ -15,6 +15,8 @@ from . import engine_common_pb2
15
15
  from . import engine_common_pb2_grpc
16
16
  from . import checkpointer_pb2
17
17
  from . import checkpointer_pb2_grpc
18
+ from . import store_pb2
19
+ from . import store_pb2_grpc
18
20
  from . import encryption_pb2
19
21
  from . import encryption_pb2_grpc
20
22
  from . import errors_pb2
@@ -27,6 +29,8 @@ __all__ = [
27
29
  "engine_common_pb2_grpc",
28
30
  "checkpointer_pb2",
29
31
  "checkpointer_pb2_grpc",
32
+ "store_pb2",
33
+ "store_pb2_grpc",
30
34
  "encryption_pb2",
31
35
  "encryption_pb2_grpc",
32
36
  "errors_pb2",
@@ -0,0 +1,54 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: store.proto
5
+ # Protobuf Python Version: 6.31.1
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 6,
15
+ 31,
16
+ 1,
17
+ '',
18
+ 'store.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+ from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
26
+
27
+
28
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bstore.proto\x12\x05store\x1a\x1bgoogle/protobuf/empty.proto\"&\n\x0c\x42\x61tchRequest\x12\x16\n\x03ops\x18\x01 \x03(\x0b\x32\t.store.Op\"1\n\rBatchResponse\x12 \n\x07results\x18\x01 \x03(\x0b\x32\x0f.store.OpResult\"D\n\x02Op\x12\x1b\n\x03get\x18\x01 \x01(\x0b\x32\x0c.store.GetOpH\x00\x12\x1b\n\x03put\x18\x02 \x01(\x0b\x32\x0c.store.PutOpH\x00\x42\x04\n\x02op\";\n\x05GetOp\x12\x11\n\tnamespace\x18\x01 \x03(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\x12\n\nrefreshTTL\x18\x03 \x01(\x08\"3\n\x0bVectorValue\x12\x11\n\tvaluePath\x18\x01 \x01(\t\x12\x11\n\tembedding\x18\x02 \x03(\x02\"t\n\x05PutOp\x12\x11\n\tnamespace\x18\x01 \x03(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\x12\n\nvalue_json\x18\x03 \x01(\x0c\x12\x12\n\nttlMinutes\x18\x04 \x01(\x05\x12#\n\x07vectors\x18\x05 \x03(\x0b\x32\x12.store.VectorValue\"Y\n\x08OpResult\x12\x1b\n\x04item\x18\x01 \x01(\x0b\x32\x0b.store.ItemH\x00\x12&\n\x04none\x18\x02 \x01(\x0b\x32\x16.google.protobuf.EmptyH\x00\x42\x08\n\x06result\"b\n\x04Item\x12\x11\n\tnamespace\x18\x01 \x03(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\x12\n\nvalue_json\x18\x03 \x01(\x0c\x12\x12\n\ncreated_at\x18\x04 \x01(\t\x12\x12\n\nupdated_at\x18\x05 \x01(\t2;\n\x05Store\x12\x32\n\x05\x42\x61tch\x12\x13.store.BatchRequest\x1a\x14.store.BatchResponseB?Z=github.com/langchain-ai/langgraph-api/core/internal/engine/pbb\x06proto3')
29
+
30
+ _globals = globals()
31
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
32
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'store_pb2', _globals)
33
+ if not _descriptor._USE_C_DESCRIPTORS:
34
+ _globals['DESCRIPTOR']._loaded_options = None
35
+ _globals['DESCRIPTOR']._serialized_options = b'Z=github.com/langchain-ai/langgraph-api/core/internal/engine/pb'
36
+ _globals['_BATCHREQUEST']._serialized_start=51
37
+ _globals['_BATCHREQUEST']._serialized_end=89
38
+ _globals['_BATCHRESPONSE']._serialized_start=91
39
+ _globals['_BATCHRESPONSE']._serialized_end=140
40
+ _globals['_OP']._serialized_start=142
41
+ _globals['_OP']._serialized_end=210
42
+ _globals['_GETOP']._serialized_start=212
43
+ _globals['_GETOP']._serialized_end=271
44
+ _globals['_VECTORVALUE']._serialized_start=273
45
+ _globals['_VECTORVALUE']._serialized_end=324
46
+ _globals['_PUTOP']._serialized_start=326
47
+ _globals['_PUTOP']._serialized_end=442
48
+ _globals['_OPRESULT']._serialized_start=444
49
+ _globals['_OPRESULT']._serialized_end=533
50
+ _globals['_ITEM']._serialized_start=535
51
+ _globals['_ITEM']._serialized_end=633
52
+ _globals['_STORE']._serialized_start=635
53
+ _globals['_STORE']._serialized_end=694
54
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,246 @@
1
+ """
2
+ @generated by mypy-protobuf. Do not edit manually!
3
+ isort:skip_file
4
+ """
5
+
6
+ from collections import abc as _abc
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import empty_pb2 as _empty_pb2
9
+ from google.protobuf import message as _message
10
+ from google.protobuf.internal import containers as _containers
11
+ import builtins as _builtins
12
+ import sys
13
+ import typing as _typing
14
+
15
+ if sys.version_info >= (3, 11):
16
+ from typing import TypeAlias as _TypeAlias, Never as _Never
17
+ else:
18
+ from typing_extensions import TypeAlias as _TypeAlias, Never as _Never
19
+
20
+ DESCRIPTOR: _descriptor.FileDescriptor
21
+
22
+ @_typing.final
23
+ class BatchRequest(_message.Message):
24
+ DESCRIPTOR: _descriptor.Descriptor
25
+
26
+ OPS_FIELD_NUMBER: _builtins.int
27
+ @_builtins.property
28
+ def ops(self) -> _containers.RepeatedCompositeFieldContainer[Global___Op]: ...
29
+ def __init__(
30
+ self,
31
+ *,
32
+ ops: _abc.Iterable[Global___Op] | None = ...,
33
+ ) -> None: ...
34
+ _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
35
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
36
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["ops", b"ops"] # noqa: Y015
37
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
38
+ def WhichOneof(self, oneof_group: _Never) -> None: ...
39
+
40
+ Global___BatchRequest: _TypeAlias = BatchRequest # noqa: Y015
41
+
42
+ @_typing.final
43
+ class BatchResponse(_message.Message):
44
+ DESCRIPTOR: _descriptor.Descriptor
45
+
46
+ RESULTS_FIELD_NUMBER: _builtins.int
47
+ @_builtins.property
48
+ def results(self) -> _containers.RepeatedCompositeFieldContainer[Global___OpResult]:
49
+ """Same length as BatchRequest.ops, same order."""
50
+
51
+ def __init__(
52
+ self,
53
+ *,
54
+ results: _abc.Iterable[Global___OpResult] | None = ...,
55
+ ) -> None: ...
56
+ _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
57
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
58
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["results", b"results"] # noqa: Y015
59
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
60
+ def WhichOneof(self, oneof_group: _Never) -> None: ...
61
+
62
+ Global___BatchResponse: _TypeAlias = BatchResponse # noqa: Y015
63
+
64
+ @_typing.final
65
+ class Op(_message.Message):
66
+ DESCRIPTOR: _descriptor.Descriptor
67
+
68
+ GET_FIELD_NUMBER: _builtins.int
69
+ PUT_FIELD_NUMBER: _builtins.int
70
+ @_builtins.property
71
+ def get(self) -> Global___GetOp: ...
72
+ @_builtins.property
73
+ def put(self) -> Global___PutOp: ...
74
+ def __init__(
75
+ self,
76
+ *,
77
+ get: Global___GetOp | None = ...,
78
+ put: Global___PutOp | None = ...,
79
+ ) -> None: ...
80
+ _HasFieldArgType: _TypeAlias = _typing.Literal["get", b"get", "op", b"op", "put", b"put"] # noqa: Y015
81
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
82
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["get", b"get", "op", b"op", "put", b"put"] # noqa: Y015
83
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
84
+ _WhichOneofReturnType_op: _TypeAlias = _typing.Literal["get", "put"] # noqa: Y015
85
+ _WhichOneofArgType_op: _TypeAlias = _typing.Literal["op", b"op"] # noqa: Y015
86
+ def WhichOneof(self, oneof_group: _WhichOneofArgType_op) -> _WhichOneofReturnType_op | None: ...
87
+
88
+ Global___Op: _TypeAlias = Op # noqa: Y015
89
+
90
+ @_typing.final
91
+ class GetOp(_message.Message):
92
+ DESCRIPTOR: _descriptor.Descriptor
93
+
94
+ NAMESPACE_FIELD_NUMBER: _builtins.int
95
+ KEY_FIELD_NUMBER: _builtins.int
96
+ REFRESHTTL_FIELD_NUMBER: _builtins.int
97
+ key: _builtins.str
98
+ refreshTTL: _builtins.bool
99
+ @_builtins.property
100
+ def namespace(self) -> _containers.RepeatedScalarFieldContainer[_builtins.str]:
101
+ """Hierarchical namespace path, e.g. ["users", "profiles"]."""
102
+
103
+ def __init__(
104
+ self,
105
+ *,
106
+ namespace: _abc.Iterable[_builtins.str] | None = ...,
107
+ key: _builtins.str = ...,
108
+ refreshTTL: _builtins.bool = ...,
109
+ ) -> None: ...
110
+ _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
111
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
112
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["key", b"key", "namespace", b"namespace", "refreshTTL", b"refreshTTL"] # noqa: Y015
113
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
114
+ def WhichOneof(self, oneof_group: _Never) -> None: ...
115
+
116
+ Global___GetOp: _TypeAlias = GetOp # noqa: Y015
117
+
118
+ @_typing.final
119
+ class VectorValue(_message.Message):
120
+ """Vector value for embedding support"""
121
+
122
+ DESCRIPTOR: _descriptor.Descriptor
123
+
124
+ VALUEPATH_FIELD_NUMBER: _builtins.int
125
+ EMBEDDING_FIELD_NUMBER: _builtins.int
126
+ valuePath: _builtins.str
127
+ """path to the value in the document"""
128
+ @_builtins.property
129
+ def embedding(self) -> _containers.RepeatedScalarFieldContainer[_builtins.float]:
130
+ """embedding vector"""
131
+
132
+ def __init__(
133
+ self,
134
+ *,
135
+ valuePath: _builtins.str = ...,
136
+ embedding: _abc.Iterable[_builtins.float] | None = ...,
137
+ ) -> None: ...
138
+ _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
139
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
140
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["embedding", b"embedding", "valuePath", b"valuePath"] # noqa: Y015
141
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
142
+ def WhichOneof(self, oneof_group: _Never) -> None: ...
143
+
144
+ Global___VectorValue: _TypeAlias = VectorValue # noqa: Y015
145
+
146
+ @_typing.final
147
+ class PutOp(_message.Message):
148
+ DESCRIPTOR: _descriptor.Descriptor
149
+
150
+ NAMESPACE_FIELD_NUMBER: _builtins.int
151
+ KEY_FIELD_NUMBER: _builtins.int
152
+ VALUE_JSON_FIELD_NUMBER: _builtins.int
153
+ TTLMINUTES_FIELD_NUMBER: _builtins.int
154
+ VECTORS_FIELD_NUMBER: _builtins.int
155
+ key: _builtins.str
156
+ value_json: _builtins.bytes
157
+ """JSON-encoded value (orjson)."""
158
+ ttlMinutes: _builtins.int
159
+ @_builtins.property
160
+ def namespace(self) -> _containers.RepeatedScalarFieldContainer[_builtins.str]: ...
161
+ @_builtins.property
162
+ def vectors(self) -> _containers.RepeatedCompositeFieldContainer[Global___VectorValue]:
163
+ """array of vector values for embedding support"""
164
+
165
+ def __init__(
166
+ self,
167
+ *,
168
+ namespace: _abc.Iterable[_builtins.str] | None = ...,
169
+ key: _builtins.str = ...,
170
+ value_json: _builtins.bytes = ...,
171
+ ttlMinutes: _builtins.int = ...,
172
+ vectors: _abc.Iterable[Global___VectorValue] | None = ...,
173
+ ) -> None: ...
174
+ _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
175
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
176
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["key", b"key", "namespace", b"namespace", "ttlMinutes", b"ttlMinutes", "value_json", b"value_json", "vectors", b"vectors"] # noqa: Y015
177
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
178
+ def WhichOneof(self, oneof_group: _Never) -> None: ...
179
+
180
+ Global___PutOp: _TypeAlias = PutOp # noqa: Y015
181
+
182
+ @_typing.final
183
+ class OpResult(_message.Message):
184
+ """Result corresponds to langgraph.store.base.Result.
185
+ Kind is derived from the originating op:
186
+ GetOp -> item (or none if missing)
187
+ PutOp -> none
188
+ """
189
+
190
+ DESCRIPTOR: _descriptor.Descriptor
191
+
192
+ ITEM_FIELD_NUMBER: _builtins.int
193
+ NONE_FIELD_NUMBER: _builtins.int
194
+ @_builtins.property
195
+ def item(self) -> Global___Item: ...
196
+ @_builtins.property
197
+ def none(self) -> _empty_pb2.Empty: ...
198
+ def __init__(
199
+ self,
200
+ *,
201
+ item: Global___Item | None = ...,
202
+ none: _empty_pb2.Empty | None = ...,
203
+ ) -> None: ...
204
+ _HasFieldArgType: _TypeAlias = _typing.Literal["item", b"item", "none", b"none", "result", b"result"] # noqa: Y015
205
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
206
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["item", b"item", "none", b"none", "result", b"result"] # noqa: Y015
207
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
208
+ _WhichOneofReturnType_result: _TypeAlias = _typing.Literal["item", "none"] # noqa: Y015
209
+ _WhichOneofArgType_result: _TypeAlias = _typing.Literal["result", b"result"] # noqa: Y015
210
+ def WhichOneof(self, oneof_group: _WhichOneofArgType_result) -> _WhichOneofReturnType_result | None: ...
211
+
212
+ Global___OpResult: _TypeAlias = OpResult # noqa: Y015
213
+
214
+ @_typing.final
215
+ class Item(_message.Message):
216
+ DESCRIPTOR: _descriptor.Descriptor
217
+
218
+ NAMESPACE_FIELD_NUMBER: _builtins.int
219
+ KEY_FIELD_NUMBER: _builtins.int
220
+ VALUE_JSON_FIELD_NUMBER: _builtins.int
221
+ CREATED_AT_FIELD_NUMBER: _builtins.int
222
+ UPDATED_AT_FIELD_NUMBER: _builtins.int
223
+ key: _builtins.str
224
+ value_json: _builtins.bytes
225
+ """JSON-encoded value (orjson)."""
226
+ created_at: _builtins.str
227
+ """RFC 3339 / ISO 8601 strings, matching Item.dict()."""
228
+ updated_at: _builtins.str
229
+ @_builtins.property
230
+ def namespace(self) -> _containers.RepeatedScalarFieldContainer[_builtins.str]: ...
231
+ def __init__(
232
+ self,
233
+ *,
234
+ namespace: _abc.Iterable[_builtins.str] | None = ...,
235
+ key: _builtins.str = ...,
236
+ value_json: _builtins.bytes = ...,
237
+ created_at: _builtins.str = ...,
238
+ updated_at: _builtins.str = ...,
239
+ ) -> None: ...
240
+ _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
241
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
242
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["created_at", b"created_at", "key", b"key", "namespace", b"namespace", "updated_at", b"updated_at", "value_json", b"value_json"] # noqa: Y015
243
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
244
+ def WhichOneof(self, oneof_group: _Never) -> None: ...
245
+
246
+ Global___Item: _TypeAlias = Item # noqa: Y015
@@ -0,0 +1,101 @@
1
+ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2
+ """Client and server classes corresponding to protobuf-defined services."""
3
+ import grpc
4
+ import warnings
5
+
6
+ from . import store_pb2 as store__pb2
7
+
8
+ GRPC_GENERATED_VERSION = '1.80.0'
9
+ GRPC_VERSION = grpc.__version__
10
+ _version_not_supported = False
11
+
12
+ try:
13
+ from grpc._utilities import first_version_is_lower
14
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
15
+ except ImportError:
16
+ _version_not_supported = True
17
+
18
+ if _version_not_supported:
19
+ raise RuntimeError(
20
+ f'The grpc package installed is at version {GRPC_VERSION},'
21
+ + ' but the generated code in store_pb2_grpc.py depends on'
22
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
23
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
24
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
25
+ )
26
+
27
+
28
+ class StoreStub(object):
29
+ """Store is the gRPC surface of langgraph's BaseStore.
30
+ """
31
+
32
+ def __init__(self, channel):
33
+ """Constructor.
34
+
35
+ Args:
36
+ channel: A grpc.Channel.
37
+ """
38
+ self.Batch = channel.unary_unary(
39
+ '/store.Store/Batch',
40
+ request_serializer=store__pb2.BatchRequest.SerializeToString,
41
+ response_deserializer=store__pb2.BatchResponse.FromString,
42
+ _registered_method=True)
43
+
44
+
45
+ class StoreServicer(object):
46
+ """Store is the gRPC surface of langgraph's BaseStore.
47
+ """
48
+
49
+ def Batch(self, request, context):
50
+ """Batch executes a sequence of operations, returning results in input order.
51
+ """
52
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
53
+ context.set_details('Method not implemented!')
54
+ raise NotImplementedError('Method not implemented!')
55
+
56
+
57
+ def add_StoreServicer_to_server(servicer, server):
58
+ rpc_method_handlers = {
59
+ 'Batch': grpc.unary_unary_rpc_method_handler(
60
+ servicer.Batch,
61
+ request_deserializer=store__pb2.BatchRequest.FromString,
62
+ response_serializer=store__pb2.BatchResponse.SerializeToString,
63
+ ),
64
+ }
65
+ generic_handler = grpc.method_handlers_generic_handler(
66
+ 'store.Store', rpc_method_handlers)
67
+ server.add_generic_rpc_handlers((generic_handler,))
68
+ server.add_registered_method_handlers('store.Store', rpc_method_handlers)
69
+
70
+
71
+ # This class is part of an EXPERIMENTAL API.
72
+ class Store(object):
73
+ """Store is the gRPC surface of langgraph's BaseStore.
74
+ """
75
+
76
+ @staticmethod
77
+ def Batch(request,
78
+ target,
79
+ options=(),
80
+ channel_credentials=None,
81
+ call_credentials=None,
82
+ insecure=False,
83
+ compression=None,
84
+ wait_for_ready=None,
85
+ timeout=None,
86
+ metadata=None):
87
+ return grpc.experimental.unary_unary(
88
+ request,
89
+ target,
90
+ '/store.Store/Batch',
91
+ store__pb2.BatchRequest.SerializeToString,
92
+ store__pb2.BatchResponse.FromString,
93
+ options,
94
+ channel_credentials,
95
+ insecure,
96
+ call_credentials,
97
+ compression,
98
+ wait_for_ready,
99
+ timeout,
100
+ metadata,
101
+ _registered_method=True)
@@ -0,0 +1,58 @@
1
+ """
2
+ @generated by mypy-protobuf. Do not edit manually!
3
+ isort:skip_file
4
+ """
5
+
6
+ from collections import abc as _abc
7
+ from grpc import aio as _aio
8
+ import abc as _abc_1
9
+ import grpc as _grpc
10
+ from . import store_pb2 as _store_pb2
11
+ import sys
12
+ import typing as _typing
13
+
14
+ if sys.version_info >= (3, 11):
15
+ from typing import Self as _Self
16
+ else:
17
+ from typing_extensions import Self as _Self
18
+
19
+ _T = _typing.TypeVar("_T")
20
+
21
+ class _MaybeAsyncIterator(_abc.AsyncIterator[_T], _abc.Iterator[_T], metaclass=_abc_1.ABCMeta): ...
22
+
23
+ class _ServicerContext(_grpc.ServicerContext, _aio.ServicerContext): # type: ignore[misc, type-arg]
24
+ ...
25
+
26
+ GRPC_GENERATED_VERSION: str
27
+ GRPC_VERSION: str
28
+
29
+ class StoreStub:
30
+ """Store is the gRPC surface of langgraph's BaseStore."""
31
+
32
+ @_typing.overload
33
+ def __new__(cls, channel: _grpc.Channel) -> _Self: ...
34
+ @_typing.overload
35
+ def __new__(cls, channel: _aio.Channel) -> StoreAsyncStub: ...
36
+ Batch: _grpc.UnaryUnaryMultiCallable[_store_pb2.BatchRequest, _store_pb2.BatchResponse]
37
+ """Batch executes a sequence of operations, returning results in input order."""
38
+
39
+ @_typing.type_check_only
40
+ class StoreAsyncStub(StoreStub):
41
+ """Store is the gRPC surface of langgraph's BaseStore."""
42
+
43
+ def __init__(self, channel: _aio.Channel) -> None: ...
44
+ Batch: _aio.UnaryUnaryMultiCallable[_store_pb2.BatchRequest, _store_pb2.BatchResponse] # type: ignore[assignment]
45
+ """Batch executes a sequence of operations, returning results in input order."""
46
+
47
+ class StoreServicer(metaclass=_abc_1.ABCMeta):
48
+ """Store is the gRPC surface of langgraph's BaseStore."""
49
+
50
+ @_abc_1.abstractmethod
51
+ def Batch(
52
+ self,
53
+ request: _store_pb2.BatchRequest,
54
+ context: _ServicerContext,
55
+ ) -> _typing.Union[_store_pb2.BatchResponse, _abc.Awaitable[_store_pb2.BatchResponse]]:
56
+ """Batch executes a sequence of operations, returning results in input order."""
57
+
58
+ def add_StoreServicer_to_server(servicer: StoreServicer, server: _typing.Union[_grpc.Server, _aio.Server]) -> None: ...
@@ -0,0 +1,109 @@
1
+ """GrpcStore — BaseStore subclass that proxies to core-server Store."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import threading
7
+ from typing import TYPE_CHECKING, Any, TypeVar
8
+
9
+ import grpc.aio
10
+ from langgraph.store.base import IndexConfig, Op, PutOp, Result
11
+ from langgraph.store.base.batch import AsyncBatchedBaseStore
12
+
13
+ from langgraph_grpc_common.conversion.store import (
14
+ normalize_index_config,
15
+ op_to_proto,
16
+ result_from_proto,
17
+ vectors_for_put_op,
18
+ )
19
+ from langgraph_grpc_common.proto import store_pb2
20
+ from langgraph_grpc_common.proto.store_pb2_grpc import StoreStub
21
+
22
+ if TYPE_CHECKING:
23
+ from collections.abc import Coroutine, Iterable
24
+
25
+ T = TypeVar("T")
26
+
27
+
28
+ class GrpcStore(AsyncBatchedBaseStore):
29
+ """BaseStore client that dispatches ``abatch`` over gRPC (get/put, TTL, vectors)."""
30
+
31
+ supports_ttl = True
32
+ _UNSUPPORTED = "delete, search, and list_namespaces are not implemented yet"
33
+
34
+ def __init__(
35
+ self,
36
+ address: str,
37
+ *,
38
+ index: IndexConfig | None = None,
39
+ ttl: dict[str, Any] | None = None,
40
+ ) -> None:
41
+ super().__init__()
42
+ self.ttl_config = ttl
43
+ self._embeddings, self._index_config = normalize_index_config(index)
44
+ self._address = address
45
+ self._channel_lock = threading.Lock()
46
+ self._channel: grpc.aio.Channel | None = None
47
+ self._stub: StoreStub | None = None
48
+
49
+ def _get_stub(self) -> StoreStub:
50
+ if self._stub is None:
51
+ with self._channel_lock:
52
+ if self._stub is None:
53
+ # Internal loopback to core-server; matches langgraph_api.grpc.client.
54
+ self._channel = grpc.aio.insecure_channel(self._address)
55
+ self._stub = StoreStub(self._channel)
56
+ return self._stub
57
+
58
+ async def abatch(self, ops: Iterable[Op]) -> list[Result]:
59
+ op_list = list(ops)
60
+ if not op_list:
61
+ return []
62
+
63
+ proto_ops: list[store_pb2.Op] = []
64
+ for op in op_list:
65
+ if (
66
+ self._index_config is not None
67
+ and self._embeddings is not None
68
+ and isinstance(op, PutOp)
69
+ and op.index is not False
70
+ ):
71
+ vectors = await vectors_for_put_op(
72
+ op, self._index_config, self._embeddings
73
+ )
74
+ proto_ops.append(op_to_proto(op, vectors=vectors))
75
+ else:
76
+ proto_ops.append(op_to_proto(op))
77
+
78
+ request = store_pb2.BatchRequest(ops=proto_ops)
79
+ stub = self._get_stub()
80
+ response = await stub.Batch(request)
81
+ if len(response.results) != len(op_list):
82
+ raise RuntimeError(
83
+ f"store-server returned {len(response.results)} results for "
84
+ f"{len(op_list)} ops"
85
+ )
86
+ return [result_from_proto(r) for r in response.results]
87
+
88
+ def batch(self, ops: Iterable[Op]) -> list[Result]:
89
+ return self._run_sync(self.abatch(ops))
90
+
91
+ def _run_sync(self, coro: Coroutine[Any, Any, T]) -> T:
92
+ return asyncio.run_coroutine_threadsafe(coro, self._loop).result()
93
+
94
+ async def aclose(self) -> None:
95
+ if self._channel is not None:
96
+ await self._channel.close()
97
+ self._channel = None
98
+ self._stub = None
99
+
100
+ async def adelete(self, namespace: tuple[str, ...], key: str) -> None:
101
+ raise NotImplementedError(self._UNSUPPORTED)
102
+
103
+ async def asearch(self, *args: Any, **kwargs: Any) -> list[Any]:
104
+ raise NotImplementedError(self._UNSUPPORTED)
105
+
106
+ async def alist_namespaces(
107
+ self, *args: Any, **kwargs: Any
108
+ ) -> list[tuple[str, ...]]:
109
+ raise NotImplementedError(self._UNSUPPORTED)