langgraph-api 0.12.0.dev15__py3-none-any.whl → 0.12.0.dev17__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.dev15"
1
+ __version__ = "0.12.0.dev17"
langgraph_api/api/meta.py CHANGED
@@ -79,6 +79,7 @@ async def meta_info(request: ApiRequest):
79
79
  "langsmith": bool(config.LANGSMITH_CONTROL_PLANE_API_KEY)
80
80
  and bool(config.TRACING),
81
81
  "langsmith_tracing_replicas": True,
82
+ "langsmith_tracing_session_on_runs": True,
82
83
  },
83
84
  "host": {
84
85
  "kind": metadata.HOST,
@@ -383,6 +383,9 @@ def proto_to_run(proto_run: pb.Run) -> Run:
383
383
  "multitask_strategy": MULTITASK_STRATEGY_FROM_PB.get(
384
384
  proto_run.multitask_strategy
385
385
  ),
386
+ "langsmith_session_name": proto_run.langsmith_session_name
387
+ if proto_run.HasField("langsmith_session_name")
388
+ else None,
386
389
  }
387
390
 
388
391
 
@@ -588,6 +591,7 @@ class Runs(Authenticated):
588
591
  if_not_exists: IfNotExists = "reject",
589
592
  after_seconds: int = 0,
590
593
  ctx: Any = None,
594
+ langsmith_session_name: str | None = None,
591
595
  ) -> AsyncIterator[Run]:
592
596
  """Create a run."""
593
597
  metadata = metadata or {}
@@ -662,6 +666,9 @@ class Runs(Authenticated):
662
666
  if after_seconds > 0:
663
667
  request_kwargs["after_seconds"] = int(after_seconds)
664
668
 
669
+ if langsmith_session_name is not None:
670
+ request_kwargs["langsmith_session_name"] = langsmith_session_name
671
+
665
672
  # Build encryption context for Go layer
666
673
  enc_ctx = build_encryption_context("run")
667
674
  if enc_ctx is not None:
@@ -10,6 +10,7 @@ import structlog
10
10
  from starlette.exceptions import HTTPException
11
11
  from typing_extensions import TypedDict
12
12
 
13
+ from langgraph_api import config as api_config
13
14
  from langgraph_api.auth.noop import UnauthenticatedUser
14
15
  from langgraph_api.encryption.middleware import encrypt_request
15
16
  from langgraph_api.encryption.shared import (
@@ -35,6 +36,7 @@ from langgraph_api.schema import (
35
36
  RunCommand,
36
37
  StreamMode,
37
38
  )
39
+ from langgraph_api.tracing_session import resolve_tracing_session_name
38
40
  from langgraph_api.utils import AsyncConnectionProto, get_auth_ctx, get_user_id
39
41
  from langgraph_api.utils.headers import get_configurable_headers
40
42
  from langgraph_api.utils.uuids import uuid7
@@ -300,6 +302,10 @@ async def create_valid_run(
300
302
  if webhook := payload.get("webhook"):
301
303
  await validate_webhook_url_or_raise(str(webhook))
302
304
 
305
+ langsmith_session_name = (
306
+ resolve_tracing_session_name(payload) if api_config.TRACING else None
307
+ )
308
+
303
309
  # We can't pass payload directly because config and context have
304
310
  # been modified above (with auth context, checkpoint info, etc.)
305
311
  fields_to_encrypt = {
@@ -346,6 +352,7 @@ async def create_valid_run(
346
352
  prevent_insert_if_inflight=prevent_insert_if_inflight,
347
353
  after_seconds=after_seconds,
348
354
  if_not_exists=if_not_exists,
355
+ langsmith_session_name=langsmith_session_name,
349
356
  )
350
357
  run_ = await run_coro
351
358
 
langgraph_api/schema.py CHANGED
@@ -255,6 +255,8 @@ class Run(TypedDict):
255
255
  """The run kwargs."""
256
256
  multitask_strategy: MultitaskStrategy
257
257
  """Strategy to handle concurrent runs on the same thread."""
258
+ langsmith_session_name: str | None
259
+ """LangSmith tracing session (project) name for this run, if tracing is enabled."""
258
260
 
259
261
 
260
262
  class RunSend(TypedDict):
@@ -359,6 +361,7 @@ RunSelectField = Literal[
359
361
  "metadata",
360
362
  "kwargs",
361
363
  "multitask_strategy",
364
+ "langsmith_session_name",
362
365
  ]
363
366
  RUN_FIELDS: set[str] = set(RunSelectField.__args__)
364
367
 
@@ -0,0 +1,22 @@
1
+ """LangSmith tracing session helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ from langsmith.utils import get_tracer_project
9
+
10
+
11
+ def resolve_tracing_session_name(payload: Mapping[str, Any]) -> str:
12
+ """Return the LangSmith session (project) name for a run.
13
+
14
+ Per-run ``langsmith_tracer.project_name`` overrides the deployment default
15
+ from ``LANGSMITH_PROJECT`` / ``LANGCHAIN_PROJECT``.
16
+ """
17
+ ls_tracing = payload.get("langsmith_tracer")
18
+ if isinstance(ls_tracing, Mapping):
19
+ project_name = ls_tracing.get("project_name")
20
+ if isinstance(project_name, str) and project_name:
21
+ return project_name
22
+ return get_tracer_project()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-api
3
- Version: 0.12.0.dev15
3
+ Version: 0.12.0.dev17
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
@@ -1,4 +1,4 @@
1
- langgraph_api/__init__.py,sha256=3hHYNZlyWnoc9JVCLo-c7OAsrbQDPhSBvtrj8CKX1z4,29
1
+ langgraph_api/__init__.py,sha256=7OXdM1PuKj42WlPEpYsH7Ln8cgRnfN3ECCN17_8jZvk,29
2
2
  langgraph_api/_factory_utils.py,sha256=5JsiJbg_YocVSryN2jwoZTg03-eyymlWMK6sKCmXwz0,5756
3
3
  langgraph_api/asgi_transport.py,sha256=XApY3lIWBZTMbbsl8dDJzl0cLGirmAGE0SifqZUnXvs,11896
4
4
  langgraph_api/asyncio.py,sha256=smqyAoO9nIyPPQRw1IVSCh85fwoZP4PEo-5dDhxGruc,10676
@@ -21,7 +21,7 @@ langgraph_api/patch.py,sha256=ViUknYvyQWS6y0f5XuaEoci2qB_mQv8vZl-oaUxsI6M,1448
21
21
  langgraph_api/queue_entrypoint.py,sha256=-9YnY_GhmDxEiGCc3k-7UqRKK_M3dPriits2iGgYlgU,11327
22
22
  langgraph_api/release_tags.py,sha256=BjgGj2vFcA7I0MDRXLw1sUA4jquz-DaKVS0Eq-dYSjE,9091
23
23
  langgraph_api/route.py,sha256=_KE8A8Q-J-QfqjGlyM2Kc6n5cirmgt8xmI5-pI8kVEE,8837
24
- langgraph_api/schema.py,sha256=I_ciXy4YE3Ri4-PAWIvqLNRH2FpC4goTkKwfFwk6wIQ,15100
24
+ langgraph_api/schema.py,sha256=4VhH4V9RmvBqO9MMPbaKk80PUQrPFyTMjDTT8nAZZ6w,15257
25
25
  langgraph_api/self_hosted_logs.py,sha256=FoUkPdtpt-nuEhejne8o1Q2phE9CccoHdoR_PvXPcBU,4442
26
26
  langgraph_api/self_hosted_metrics.py,sha256=pWsQQ-2ukoFIbmVfzNOSkwCqZ5Cnts6pRSWTII44Ll4,16844
27
27
  langgraph_api/serde.py,sha256=V3fO9bkUOlBX3okw5Qi31nlcr59fcuXMgL7DHNyarZY,8855
@@ -32,6 +32,7 @@ langgraph_api/store.py,sha256=i-jVfWzDEmPESVofh1tNWbo0HMLiLz_BSKKV4QjN-dE,5027
32
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
+ langgraph_api/tracing_session.py,sha256=gmfv-BW1XRar6suVur1P4rTVKWkjElDEb2hhkBduWOs,728
35
36
  langgraph_api/validation.py,sha256=XyeKyt7jAICmIlT_b0J0mv2YbwIbNoe4m6zEmfk9gOA,14657
36
37
  langgraph_api/webhook.py,sha256=qXEtkE6orek2POeOQmPRsEarJgXIYp-LBrZB-OwITxc,9572
37
38
  langgraph_api/worker.py,sha256=HGirbQ1i1hxaayORoihsQ6JMVAq3VsrC8CUxAHwY9t0,21151
@@ -42,7 +43,7 @@ langgraph_api/api/__init__.py,sha256=Zu1ew3dxYZu7cLRAjn-6HcYmtuQBdihlVFMKMJ77Y3c
42
43
  langgraph_api/api/a2a.py,sha256=VPllgqfoLUQD6Eqob3RjcegjtKgLhphNGTrTqbNLoIY,95135
43
44
  langgraph_api/api/assistants.py,sha256=4v1TpkeeSF7vFrbnOKIvh7BY4K0WamzEdMeTAzwRElE,20786
44
45
  langgraph_api/api/event_streaming.py,sha256=nvoaKz4QGklX5YUmY9WQ3vSwhQ1Q81QeQWNR8aEXUz8,17571
45
- langgraph_api/api/meta.py,sha256=4vqfgJYIqaqwZPfdmvHN00Co7SdeYSnQ1STxyvGWVDw,4698
46
+ langgraph_api/api/meta.py,sha256=5s017GfMs-Ghq6Z5K6yW4UjbVFzCsmeXKn4UEJmfs98,4757
46
47
  langgraph_api/api/openapi.py,sha256=Zkdlb9mjrQyHro1TtrDIWVuaBDovxx-uGWJ1fZMOg54,12604
47
48
  langgraph_api/api/profile.py,sha256=CA1ZkHALOuP8orYTICnEhcG_JnnA2wnyjbWyeb117jA,3455
48
49
  langgraph_api/api/runs.py,sha256=h5droLgaz_aAyILCRJIpbj2KH1PbijCeXcggOSa3Zww,35178
@@ -90,7 +91,7 @@ langgraph_api/grpc/ops/__init__.py,sha256=ACO-Cp7ygehNfOG1ucSH8iMw3hVXbLluxiqczG
90
91
  langgraph_api/grpc/ops/assistants.py,sha256=00vY7Dd7BcXaVp_6UrKZIb5b4tQ8Lm5MxJcEpyVPfo0,13902
91
92
  langgraph_api/grpc/ops/cache.py,sha256=MoPAmSuOwbt2Cud1YZJm_YJeaDHPFy1IiV4m0PDImgg,1399
92
93
  langgraph_api/grpc/ops/crons.py,sha256=oGPW9qW-J4H9baX7Jsee1opsFnXfThHLHh_N47mDf9s,19596
93
- langgraph_api/grpc/ops/runs.py,sha256=dD2Rne9EBU7z4lGF3OicpIMZNU4MBNbbli1Hw5gz2tw,41479
94
+ langgraph_api/grpc/ops/runs.py,sha256=4-Mp-4MgDAn8uerLrOpmXuUqOdfcnzSmb3hSRlO8HhM,41798
94
95
  langgraph_api/grpc/ops/threads.py,sha256=SJraV838nZ0m6wRrCia7sLYh8KK1WfY7x5xmcspZLek,48140
95
96
  langgraph_api/grpc/servicers/__init__.py,sha256=l8yqTA_gMbIj0xHw5-RZYo0JbjL-EpFD8RxPMgW_-bA,422
96
97
  langgraph_api/grpc/servicers/checkpointer.py,sha256=aeB2hzsDxbb9i-ildx-kylpDxaYtpAUSG6rphOzbDOs,10482
@@ -130,7 +131,7 @@ langgraph_api/middleware/http_logger.py,sha256=jjqLBPqoGRC1UfB2VYKPY2tkq6gT7Rm88
130
131
  langgraph_api/middleware/private_network.py,sha256=eQEzWI8epBNUCiNsMu9O27ofHBQ45M0p2OZy5YdUYos,2097
131
132
  langgraph_api/middleware/request_id.py,sha256=-p230Q5jDJAJLmSZRqQvB4dFFkJS9B4Vwg6pUgQtI24,1259
132
133
  langgraph_api/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
133
- langgraph_api/models/run.py,sha256=LfL8nPLPVA0dZMXA_hzfl2GKsZGyPiVW36B0MbbSosc,15793
134
+ langgraph_api/models/run.py,sha256=1jA7HywYT95ekz_gMBkR0dnzR_1gdl5vhqDaackAlwA,16082
134
135
  langgraph_api/timing/__init__.py,sha256=lWXHCGh71I8ouF0wUznYRMKHAQ-8VQHF9gf9jkeV5dg,526
135
136
  langgraph_api/timing/profiler.py,sha256=_n0qJYJMjCGo95RDuH5z8FIzePGNSNbnm3cvGmcgn6A,6186
136
137
  langgraph_api/timing/timer.py,sha256=lC_xu4rlejkFPxqV6k_FD4dWtjpT9cEICBP333hAxTg,9404
@@ -161,17 +162,17 @@ langgraph_runtime/store.py,sha256=7mowndlsIroGHv3NpTSOZDJR0lCuaYMBoTnTrewjslw,11
161
162
  LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
162
163
  hatch_build.py,sha256=Nn9N-EFfVqmH5q3iCUrhqX6zc7qT1jLI3gWSBN2xgYQ,1738
163
164
  logging.json,sha256=sBy8HDDPuucpLbLUD6e8t5fsiQjJoklZsbVHi2qQ7aQ,367
164
- openapi.json,sha256=0N-D4j0_3ruH6nMF4k5lnqmpV-aoB5Se-WWheCQuQQs,219552
165
+ openapi.json,sha256=_gcvdkH6zWjzvEZ4YPGpJ0AMzTwM1cbJlpVgJSxzM8I,219880
165
166
  langgraph_grpc_common/__init__.py,sha256=sO7jFHekQxt6zZPhTPZAWLYLB_S8w7-VuJ7NyWFlFNo,97
166
167
  langgraph_grpc_common/checkpointer.py,sha256=u3ERFgqnUCpCYpOSxKR_6Jvo_tR4yDvq7qU-EL61bSg,14022
167
168
  langgraph_grpc_common/serde.py,sha256=pxIhgxtrXtwWVhAlLE2LAeSAuKk2ZHtnvAP3DRDT2Qw,6184
168
- langgraph_grpc_common/store.py,sha256=BIHYT_BTw2ByFAIsomu9vwNVu1kZdUEOjkJChV7rjuA,3721
169
+ langgraph_grpc_common/store.py,sha256=SizRCSOoNVbCHzsNuyuzC9dQdKi4x4WOV5S7pqYWdp8,3846
169
170
  langgraph_grpc_common/conversion/__init__.py,sha256=pLsOrRtmF9YkgmCPhcfym6lYOB7PCJTRItyo1YK_AT8,208
170
171
  langgraph_grpc_common/conversion/_compat.py,sha256=MIEo69I7tfaPskunPGwBCKheksLwxpw-z-4O3hvO9og,4010
171
172
  langgraph_grpc_common/conversion/checkpoint.py,sha256=Ty81cijafeal7blzzXbYFjWpu9iXVb_spFLBZ2FvmgI,12045
172
173
  langgraph_grpc_common/conversion/config.py,sha256=HjlHRX-_GpKa1m0RWVL1PEZQCvA3w_8wKVYrtaT06do,13015
173
174
  langgraph_grpc_common/conversion/durability.py,sha256=FWhqjzVN4JrrsAL9AwsJkvIorQGNRl14w97Va-WZRk0,991
174
- langgraph_grpc_common/conversion/store.py,sha256=NGGZp4FiMzJXByIqRRy2qYDxlWeXBGkEN9k1aNTsVUA,6253
175
+ langgraph_grpc_common/conversion/store.py,sha256=ctWDdgpJHEkkCeguWSwmISlS8OJLJLP9srwib-Hi_OE,15211
175
176
  langgraph_grpc_common/conversion/struct.py,sha256=yk4jpPdywzn8UosAohun21lU5D_IIglOY9rkBhiG-gA,2072
176
177
  langgraph_grpc_common/conversion/value.py,sha256=eZ9fDqKje9tKsO8A_6f0BPSI7lt94Vzuu3NNEEYLFBM,8343
177
178
  langgraph_grpc_common/proto/__init__.py,sha256=eBejrrULiDdasi3_Hga_AJW8ZPMkjKyyAeW9Ey640Ks,1375
@@ -179,8 +180,8 @@ langgraph_grpc_common/proto/checkpointer_pb2.py,sha256=c-gaVwNHm7sUzThJDzumME2M_
179
180
  langgraph_grpc_common/proto/checkpointer_pb2.pyi,sha256=rGUxQyNFifmu8OPy0S418sCWSBQY_CVckobPXtFfZfs,20763
180
181
  langgraph_grpc_common/proto/checkpointer_pb2_grpc.py,sha256=MLhZ7M4g9gjfXZqWenk93oO2uIZSybbFoaeU8bauFPA,20176
181
182
  langgraph_grpc_common/proto/checkpointer_pb2_grpc.pyi,sha256=8fPL90WjHnMaREG7l9qYt5fQePeT8YFWQ0qVy56QtYw,10210
182
- langgraph_grpc_common/proto/core_api_pb2.py,sha256=xbzqAKhwvgc_-J5BlqS7krhOlgKsRgzrb7uhGax1jbo,50813
183
- langgraph_grpc_common/proto/core_api_pb2.pyi,sha256=lN5P83sM9_gvlPGOkDQIUEq9QDiiffmY2MIQOZtdV7s,206457
183
+ langgraph_grpc_common/proto/core_api_pb2.py,sha256=ggBZW9ywqm2Y4lgpWURuxd0bfit_JUMb1zSZ11UMylc,51014
184
+ langgraph_grpc_common/proto/core_api_pb2.pyi,sha256=1AtbsmQcOzwYSODInI9mEioWcBCtcc3VNBra7FEXICw,207984
184
185
  langgraph_grpc_common/proto/core_api_pb2_grpc.py,sha256=zIfWY664xF4r9c48Ys1OcYN4dEmtIWAwYiCzP7gsyLI,81342
185
186
  langgraph_grpc_common/proto/core_api_pb2_grpc.pyi,sha256=XLu2-5YZHm9QyZWjWsnUWe7uN6n1-avVvsyR6N5D3fs,29325
186
187
  langgraph_grpc_common/proto/encryption_pb2.py,sha256=W9Zs5gclBzKfwHfKDB_83tFfabR9_KK2Lq81xVYleMQ,3355
@@ -231,12 +232,12 @@ langgraph_grpc_common/proto/errors_pb2.py,sha256=JI6x-vBK1AE7DHZ5DQwN1mZWF6C4xTR
231
232
  langgraph_grpc_common/proto/errors_pb2.pyi,sha256=rd3-BYUH8V-aO66taL7OOblaLgdrDtf1Vcd38GUoVVM,2181
232
233
  langgraph_grpc_common/proto/errors_pb2_grpc.py,sha256=2-LwQ0OPGo-NtC0269q7Fw6GPBxnTLYWq3xP5Eq0_YA,886
233
234
  langgraph_grpc_common/proto/errors_pb2_grpc.pyi,sha256=uC9Wnq6uyg488QiONpJ0ba1s_iouQCOYsjd_FDd1XUM,495
234
- langgraph_grpc_common/proto/store_pb2.py,sha256=5px40nIUag8p0KlE6o5vpvgfenX71BDIr3VjFfFQP6E,3406
235
- langgraph_grpc_common/proto/store_pb2.pyi,sha256=cZZGod9tBReUukPkjNzssEdz5OBoZ2pecXugbp-9Lmo,9755
235
+ langgraph_grpc_common/proto/store_pb2.py,sha256=uAYM7OcdEOK12iRcPYihC2suSeCo_QdbpNSWfDIkXf8,7652
236
+ langgraph_grpc_common/proto/store_pb2.pyi,sha256=_dcxyxV8moR6H67qU42r2h2p_Ub-PZSHs3OoS9KMdSY,27782
236
237
  langgraph_grpc_common/proto/store_pb2_grpc.py,sha256=wSD65QgFkYTkPTHcf7VhyI2FQ7UH_RgIf3vQW3VYlsM,3324
237
238
  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,,
239
+ langgraph_api-0.12.0.dev17.dist-info/METADATA,sha256=7Ir52CwtjuY0ASAp6p09CkQVEUvxfqmb9mQoN7o-cgE,4631
240
+ langgraph_api-0.12.0.dev17.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
241
+ langgraph_api-0.12.0.dev17.dist-info/entry_points.txt,sha256=hGedv8n7cgi41PypMfinwS_HfCwA7xJIfS0jAp8htV8,78
242
+ langgraph_api-0.12.0.dev17.dist-info/licenses/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
243
+ langgraph_api-0.12.0.dev17.dist-info/RECORD,,
@@ -11,9 +11,13 @@ from langgraph.store.base import (
11
11
  GetOp,
12
12
  IndexConfig,
13
13
  Item,
14
+ ListNamespacesOp,
15
+ MatchCondition,
14
16
  Op,
15
17
  PutOp,
16
18
  Result,
19
+ SearchItem,
20
+ SearchOp,
17
21
  ensure_embeddings,
18
22
  get_text_at_path,
19
23
  tokenize_path,
@@ -64,10 +68,92 @@ def _ttl_to_proto_minutes(ttl: float | None) -> int:
64
68
  return int(ttl)
65
69
 
66
70
 
71
+ # Ordered comparisons compare the *text* form of the field (value->>key); $eq/$ne
72
+ # are handled separately as jsonb comparisons.
73
+ _FILTER_OP_BY_NAME = {
74
+ "$gt": store_pb2.FILTER_OP_GT,
75
+ "$gte": store_pb2.FILTER_OP_GTE,
76
+ "$lt": store_pb2.FILTER_OP_LT,
77
+ "$lte": store_pb2.FILTER_OP_LTE,
78
+ }
79
+
80
+ # pgvector distance / column types come from the index config as strings; map
81
+ # them to the wire enums (see store.proto DistanceType / VectorType).
82
+ _DISTANCE_TYPE_BY_NAME = {
83
+ "cosine": store_pb2.DISTANCE_TYPE_COSINE,
84
+ "l2": store_pb2.DISTANCE_TYPE_L2,
85
+ "inner_product": store_pb2.DISTANCE_TYPE_INNER_PRODUCT,
86
+ }
87
+ _VECTOR_TYPE_BY_NAME = {
88
+ "vector": store_pb2.VECTOR_TYPE_VECTOR,
89
+ "halfvec": store_pb2.VECTOR_TYPE_HALFVEC,
90
+ }
91
+
92
+ # ListNamespaces match type: langgraph uses the strings "prefix"/"suffix".
93
+ _MATCH_TYPE_BY_NAME = {
94
+ "prefix": store_pb2.MATCH_TYPE_PREFIX,
95
+ "suffix": store_pb2.MATCH_TYPE_SUFFIX,
96
+ }
97
+ _MATCH_TYPE_TO_NAME = {op: name for name, op in _MATCH_TYPE_BY_NAME.items()}
98
+
99
+
100
+ def _match_type_to_proto(match_type: str) -> int:
101
+ proto = _MATCH_TYPE_BY_NAME.get(match_type)
102
+ if proto is None:
103
+ raise ValueError(f"Unsupported list_namespaces match_type: {match_type!r}")
104
+ return proto
105
+
106
+
107
+ def _filter_to_conditions(
108
+ filter_dict: dict[str, Any],
109
+ ) -> list[store_pb2.FilterCondition]:
110
+ """Flatten a langgraph filter dict into typed FilterConditions.
111
+
112
+ A plain value is an equality match; a dict is a set of operator comparisons.
113
+ $eq/$ne compare as jsonb; $gt/$gte/$lt/$lte compare on the text form of the
114
+ value (str(value), produced here so the store never re-derives it). Mirrors
115
+ OSS PostgresStore._get_filter_condition, done in Python where the DSL is
116
+ native so the store only maps op -> SQL.
117
+ """
118
+ conditions: list[store_pb2.FilterCondition] = []
119
+ for key, value in filter_dict.items():
120
+ if isinstance(value, dict):
121
+ for op_name, op_val in value.items():
122
+ if op_name in ("$eq", "$ne"):
123
+ op = (
124
+ store_pb2.FILTER_OP_EQ
125
+ if op_name == "$eq"
126
+ else store_pb2.FILTER_OP_NE
127
+ )
128
+ conditions.append(
129
+ store_pb2.FilterCondition(
130
+ key=key, op=op, operand=orjson.dumps(op_val).decode()
131
+ )
132
+ )
133
+ elif op_name in _FILTER_OP_BY_NAME:
134
+ conditions.append(
135
+ store_pb2.FilterCondition(
136
+ key=key, op=_FILTER_OP_BY_NAME[op_name], operand=str(op_val)
137
+ )
138
+ )
139
+ else:
140
+ raise ValueError(f"Unsupported filter operator: {op_name}")
141
+ else:
142
+ conditions.append(
143
+ store_pb2.FilterCondition(
144
+ key=key,
145
+ op=store_pb2.FILTER_OP_EQ,
146
+ operand=orjson.dumps(value).decode(),
147
+ )
148
+ )
149
+ return conditions
150
+
151
+
67
152
  def op_to_proto(
68
153
  op: Op,
69
154
  *,
70
155
  vectors: Sequence[store_pb2.VectorValue] | None = None,
156
+ search_vector: store_pb2.VectorSearch | None = None,
71
157
  ) -> store_pb2.Op:
72
158
  if isinstance(op, GetOp):
73
159
  return store_pb2.Op(
@@ -78,9 +164,14 @@ def op_to_proto(
78
164
  )
79
165
  )
80
166
  if isinstance(op, PutOp):
167
+ # A PutOp with value=None is a delete in OSS; on the gRPC path it maps to
168
+ # an explicit DeleteOp so an empty value can't be mistaken for a delete.
81
169
  if op.value is None:
82
- raise TypeError(
83
- "PutOp with value=None (delete) is not supported by gRPC store"
170
+ return store_pb2.Op(
171
+ delete=store_pb2.DeleteOp(
172
+ namespace=list(op.namespace),
173
+ key=str(op.key),
174
+ )
84
175
  )
85
176
  put_kwargs: dict[str, Any] = {
86
177
  "namespace": list(op.namespace),
@@ -91,6 +182,36 @@ def op_to_proto(
91
182
  if vectors is not None:
92
183
  put_kwargs["vectors"] = list(vectors)
93
184
  return store_pb2.Op(put=store_pb2.PutOp(**put_kwargs))
185
+ if isinstance(op, SearchOp):
186
+ # The natural-language query itself is not sent: Python embeds it into
187
+ # `search_vector`, which is the only semantic-search signal the store needs.
188
+ search_kwargs: dict[str, Any] = {
189
+ "namespace_prefix": list(op.namespace_prefix),
190
+ "limit": op.limit,
191
+ "offset": op.offset,
192
+ "refreshTTL": op.refresh_ttl,
193
+ }
194
+ if op.filter:
195
+ search_kwargs["filter"] = _filter_to_conditions(op.filter)
196
+ if search_vector is not None:
197
+ search_kwargs["vector"] = search_vector
198
+ return store_pb2.Op(search=store_pb2.SearchOp(**search_kwargs))
199
+ if isinstance(op, ListNamespacesOp):
200
+ list_kwargs: dict[str, Any] = {
201
+ "limit": op.limit,
202
+ "offset": op.offset,
203
+ }
204
+ if op.match_conditions:
205
+ list_kwargs["match_conditions"] = [
206
+ store_pb2.MatchCondition(
207
+ match_type=_match_type_to_proto(mc.match_type),
208
+ path=list(mc.path),
209
+ )
210
+ for mc in op.match_conditions
211
+ ]
212
+ if op.max_depth is not None:
213
+ list_kwargs["max_depth"] = op.max_depth
214
+ return store_pb2.Op(list_namespaces=store_pb2.ListNamespacesOp(**list_kwargs))
94
215
  raise TypeError(f"unsupported op: {type(op).__name__}")
95
216
 
96
217
 
@@ -114,6 +235,35 @@ def op_from_proto(msg: store_pb2.Op) -> Op:
114
235
  value=orjson.loads(p.value_json),
115
236
  ttl=ttl,
116
237
  )
238
+ if kind == "delete":
239
+ d = msg.delete
240
+ return PutOp(namespace=tuple(d.namespace), key=d.key, value=None)
241
+ if kind == "search":
242
+ s = msg.search
243
+ # `query` (embedded into `vector`) and `filter` (flattened to typed
244
+ # conditions) are one-directional encodings, not reconstructed here; the
245
+ # store consumes them and Python never decodes a SearchOp in production.
246
+ return SearchOp(
247
+ namespace_prefix=tuple(s.namespace_prefix),
248
+ limit=s.limit,
249
+ offset=s.offset,
250
+ refresh_ttl=s.refreshTTL,
251
+ )
252
+ if kind == "list_namespaces":
253
+ ln = msg.list_namespaces
254
+ return ListNamespacesOp(
255
+ match_conditions=tuple(
256
+ MatchCondition(
257
+ match_type=_MATCH_TYPE_TO_NAME[mc.match_type],
258
+ path=tuple(mc.path),
259
+ )
260
+ for mc in ln.match_conditions
261
+ )
262
+ or None,
263
+ max_depth=ln.max_depth if ln.HasField("max_depth") else None,
264
+ limit=ln.limit,
265
+ offset=ln.offset,
266
+ )
117
267
  raise ValueError("empty Op message (no oneof set)")
118
268
 
119
269
 
@@ -128,6 +278,20 @@ def result_to_proto(result: Result, op: Op) -> store_pb2.OpResult:
128
278
  return store_pb2.OpResult(item=_item_to_proto(result))
129
279
  if isinstance(op, PutOp):
130
280
  return store_pb2.OpResult(none=empty_pb2.Empty())
281
+ if isinstance(op, SearchOp):
282
+ items = cast("list[SearchItem]", result or [])
283
+ return store_pb2.OpResult(
284
+ search=store_pb2.SearchResult(
285
+ items=[_search_item_to_proto(item) for item in items]
286
+ )
287
+ )
288
+ if isinstance(op, ListNamespacesOp):
289
+ namespaces = cast("list[tuple[str, ...]]", result or [])
290
+ return store_pb2.OpResult(
291
+ list_namespaces=store_pb2.ListNamespacesResult(
292
+ namespaces=[store_pb2.Namespace(parts=list(ns)) for ns in namespaces]
293
+ )
294
+ )
131
295
  raise TypeError(f"unsupported op for result encoding: {type(op).__name__}")
132
296
 
133
297
 
@@ -137,6 +301,10 @@ def result_from_proto(msg: store_pb2.OpResult) -> Result:
137
301
  return None
138
302
  if kind == "item":
139
303
  return _item_from_proto(msg.item)
304
+ if kind == "search":
305
+ return [_search_item_from_proto(item) for item in msg.search.items]
306
+ if kind == "list_namespaces":
307
+ return [tuple(ns.parts) for ns in msg.list_namespaces.namespaces]
140
308
  raise ValueError("empty OpResult message (no oneof set)")
141
309
 
142
310
 
@@ -160,6 +328,30 @@ def _item_from_proto(msg: store_pb2.Item) -> Item:
160
328
  )
161
329
 
162
330
 
331
+ def _search_item_to_proto(item: SearchItem) -> store_pb2.SearchItem:
332
+ kwargs: dict[str, Any] = {
333
+ "namespace": list(item.namespace),
334
+ "key": item.key,
335
+ "value_json": orjson.dumps(item.value),
336
+ "created_at": _format_dt(item.created_at),
337
+ "updated_at": _format_dt(item.updated_at),
338
+ }
339
+ if item.score is not None:
340
+ kwargs["score"] = item.score
341
+ return store_pb2.SearchItem(**kwargs)
342
+
343
+
344
+ def _search_item_from_proto(msg: store_pb2.SearchItem) -> SearchItem:
345
+ return SearchItem(
346
+ namespace=tuple(msg.namespace),
347
+ key=msg.key,
348
+ value=orjson.loads(msg.value_json),
349
+ created_at=datetime.fromisoformat(msg.created_at),
350
+ updated_at=datetime.fromisoformat(msg.updated_at),
351
+ score=msg.score if msg.HasField("score") else None,
352
+ )
353
+
354
+
163
355
  def _format_dt(dt: datetime) -> str:
164
356
  return dt.isoformat()
165
357
 
@@ -200,6 +392,48 @@ async def vectors_for_put_op(
200
392
  ]
201
393
 
202
394
 
395
+ async def query_embedding_for_search_op(
396
+ op: SearchOp,
397
+ index_config: IndexConfig,
398
+ embeddings: Embeddings,
399
+ ) -> store_pb2.VectorSearch | None:
400
+ """Embed a SearchOp's natural-language query for semantic search.
401
+
402
+ Returns None when the op has no query (a plain filter/pagination search).
403
+ Distance params are read from the index config and sent alongside the
404
+ embedding so the Go store stays stateless.
405
+ """
406
+ if not op.query:
407
+ return None
408
+
409
+ vectors = await embeddings.aembed_documents([op.query])
410
+ if not vectors:
411
+ return None
412
+
413
+ cfg = cast("dict[str, Any]", index_config)
414
+ distance_type = cfg.get("distance_type", "cosine")
415
+ vectors_per_doc = cfg.get("__estimated_num_vectors", 1)
416
+ vector_type = cfg.get("ann_index_config", {}).get("vector_type", "vector")
417
+
418
+ if distance_type not in _DISTANCE_TYPE_BY_NAME:
419
+ raise ValueError(
420
+ f"gRPC store semantic search does not support distance_type={distance_type!r} "
421
+ "(only 'cosine', 'l2', 'inner_product')"
422
+ )
423
+ if vector_type not in _VECTOR_TYPE_BY_NAME:
424
+ raise NotImplementedError(
425
+ f"gRPC store semantic search does not support vector_type={vector_type!r} "
426
+ "(only 'vector' and 'halfvec')"
427
+ )
428
+
429
+ return store_pb2.VectorSearch(
430
+ query_embedding=vectors[0],
431
+ distance_type=_DISTANCE_TYPE_BY_NAME[distance_type],
432
+ vectors_per_doc=vectors_per_doc,
433
+ vector_type=_VECTOR_TYPE_BY_NAME[vector_type],
434
+ )
435
+
436
+
203
437
  def normalize_index_config(
204
438
  index: IndexConfig | None,
205
439
  ) -> tuple[Embeddings | None, IndexConfig | None]: