langgraph-api 0.0.31__py3-none-any.whl → 0.0.32__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.

Potentially problematic release.


This version of langgraph-api might be problematic. Click here for more details.

@@ -87,7 +87,13 @@ def _graph_schemas(graph: Pregel) -> dict:
87
87
  f"Failed to get output schema for graph {graph.name} with error: `{str(e)}`"
88
88
  )
89
89
  output_schema = None
90
- state_schema = _state_jsonschema(graph)
90
+ try:
91
+ state_schema = _state_jsonschema(graph)
92
+ except Exception as e:
93
+ logger.warning(
94
+ f"Failed to get state schema for graph {graph.name} with error: `{str(e)}`"
95
+ )
96
+ state_schema = None
91
97
  try:
92
98
  config_schema = _get_configurable_jsonschema(graph)
93
99
  except Exception as e:
@@ -28,12 +28,21 @@ async def create_thread(
28
28
  if thread_id := payload.get("thread_id"):
29
29
  validate_uuid(thread_id, "Invalid thread ID: must be a UUID")
30
30
  async with connect() as conn:
31
+ thread_id = thread_id or str(uuid4())
31
32
  iter = await Threads.put(
32
33
  conn,
33
- thread_id or uuid4(),
34
+ thread_id,
34
35
  metadata=payload.get("metadata"),
35
36
  if_exists=payload.get("if_exists") or "raise",
36
37
  )
38
+
39
+ if supersteps := payload.get("supersteps"):
40
+ await Threads.State.bulk(
41
+ conn,
42
+ config={"configurable": {"thread_id": thread_id}},
43
+ supersteps=supersteps,
44
+ )
45
+
37
46
  return ApiResponse(await fetchone(iter, not_found_code=409))
38
47
 
39
48
 
langgraph_api/cli.py CHANGED
@@ -129,6 +129,7 @@ def run_server(
129
129
  store: typing.Optional["StoreConfig"] = None,
130
130
  auth: AuthConfig | None = None,
131
131
  http: typing.Optional["HttpConfig"] = None,
132
+ studio_url: str | None = None,
132
133
  **kwargs: typing.Any,
133
134
  ):
134
135
  """Run the LangGraph API server."""
@@ -192,11 +193,11 @@ def run_server(
192
193
  ALLOW_PRIVATE_NETWORK="true",
193
194
  **(env_vars or {}),
194
195
  ):
195
- studio_origin = _get_ls_origin() or "https://smith.langchain.com"
196
- studio_url = f"{studio_origin}/studio/?baseUrl={local_url}"
196
+ studio_origin = studio_url or _get_ls_origin() or "https://smith.langchain.com"
197
+ full_studio_url = f"{studio_origin}/studio/?baseUrl={local_url}"
197
198
 
198
199
  def _open_browser():
199
- nonlocal studio_origin, studio_url
200
+ nonlocal studio_origin, full_studio_url
200
201
  import time
201
202
  import urllib.request
202
203
  import webbrowser
@@ -218,7 +219,7 @@ def run_server(
218
219
  try:
219
220
  org_id = org_id_future.result(timeout=3.0)
220
221
  if org_id:
221
- studio_url = f"{studio_origin}/studio/?baseUrl={local_url}&organizationId={org_id}"
222
+ full_studio_url = f"{studio_origin}/studio/?baseUrl={local_url}&organizationId={org_id}"
222
223
  except TimeoutError as e:
223
224
  thread_logger.debug(
224
225
  f"Failed to get organization ID: {str(e)}"
@@ -230,8 +231,8 @@ def run_server(
230
231
  thread_logger.info(
231
232
  "🎨 Opening Studio in your browser..."
232
233
  )
233
- thread_logger.info("URL: " + studio_url)
234
- webbrowser.open(studio_url)
234
+ thread_logger.info("URL: " + full_studio_url)
235
+ webbrowser.open(full_studio_url)
235
236
  return
236
237
  except urllib.error.URLError:
237
238
  pass
@@ -246,7 +247,7 @@ def run_server(
246
247
  ╩═╝┴ ┴┘└┘└─┘╚═╝┴└─┴ ┴┴ ┴ ┴
247
248
 
248
249
  - 🚀 API: \033[36m{local_url}\033[0m
249
- - 🎨 Studio UI: \033[36m{studio_url}\033[0m
250
+ - 🎨 Studio UI: \033[36m{full_studio_url}\033[0m
250
251
  - 📚 API Docs: \033[36m{local_url}/docs\033[0m
251
252
 
252
253
  This in-memory server is designed for development and testing.
@@ -0,0 +1,29 @@
1
+ from langgraph.types import Command, Send
2
+
3
+ from langgraph_api.schema import RunCommand
4
+
5
+
6
+ def map_cmd(cmd: RunCommand) -> Command:
7
+ goto = cmd.get("goto")
8
+ if goto is not None and not isinstance(goto, list):
9
+ goto = [cmd.get("goto")]
10
+
11
+ update = cmd.get("update")
12
+ if isinstance(update, tuple | list) and all(
13
+ isinstance(t, tuple | list) and len(t) == 2 and isinstance(t[0], str)
14
+ for t in update
15
+ ):
16
+ update = [tuple(t) for t in update]
17
+
18
+ return Command(
19
+ update=update,
20
+ goto=(
21
+ [
22
+ it if isinstance(it, str) else Send(it["node"], it["input"])
23
+ for it in goto
24
+ ]
25
+ if goto
26
+ else None
27
+ ),
28
+ resume=cmd.get("resume"),
29
+ )
@@ -1,8 +1,10 @@
1
1
  import asyncio
2
+ import logging
2
3
  import os
3
4
  import shutil
4
5
  import ssl
5
6
  from collections.abc import AsyncIterator
7
+ from contextlib import AbstractContextManager
6
8
  from typing import Any, Literal
7
9
 
8
10
  import certifi
@@ -605,60 +607,79 @@ async def run_remote_checkpointer():
605
607
  await server.serve()
606
608
 
607
609
 
610
+ class DisableHttpxLoggingContextManager(AbstractContextManager):
611
+ """
612
+ Disable HTTP/1.1 200 OK logs spamming stdout.
613
+ """
614
+
615
+ filter: logging.Filter
616
+
617
+ def filter(self, record: logging.LogRecord) -> bool:
618
+ return "200 OK" not in record.getMessage()
619
+
620
+ def __enter__(self):
621
+ logging.getLogger("httpx").addFilter(self.filter)
622
+
623
+ def __exit__(self, exc_type, exc_value, traceback):
624
+ logging.getLogger("httpx").removeFilter(self.filter)
625
+
626
+
608
627
  async def wait_until_js_ready():
609
- async with (
610
- httpx.AsyncClient(
611
- base_url=f"http://localhost:{GRAPH_PORT}",
612
- limits=httpx.Limits(max_connections=1),
613
- transport=httpx.AsyncHTTPTransport(verify=SSL),
614
- ) as graph_client,
615
- httpx.AsyncClient(
616
- base_url=f"http://localhost:{REMOTE_PORT}",
617
- limits=httpx.Limits(max_connections=1),
618
- transport=httpx.AsyncHTTPTransport(verify=SSL),
619
- ) as checkpointer_client,
620
- ):
621
- attempt = 0
622
- while not asyncio.current_task().cancelled():
628
+ with DisableHttpxLoggingContextManager():
629
+ async with (
630
+ httpx.AsyncClient(
631
+ base_url=f"http://localhost:{GRAPH_PORT}",
632
+ limits=httpx.Limits(max_connections=1),
633
+ transport=httpx.AsyncHTTPTransport(verify=SSL),
634
+ ) as graph_client,
635
+ httpx.AsyncClient(
636
+ base_url=f"http://localhost:{REMOTE_PORT}",
637
+ limits=httpx.Limits(max_connections=1),
638
+ transport=httpx.AsyncHTTPTransport(verify=SSL),
639
+ ) as checkpointer_client,
640
+ ):
641
+ attempt = 0
642
+ while not asyncio.current_task().cancelled():
643
+ try:
644
+ res = await graph_client.get("/ok")
645
+ res.raise_for_status()
646
+ res = await checkpointer_client.get("/ok")
647
+ res.raise_for_status()
648
+ return
649
+ except httpx.HTTPError:
650
+ if attempt > 240:
651
+ raise
652
+ else:
653
+ attempt += 1
654
+ await asyncio.sleep(0.5)
655
+
656
+
657
+ async def js_healthcheck():
658
+ with DisableHttpxLoggingContextManager():
659
+ async with (
660
+ httpx.AsyncClient(
661
+ base_url=f"http://localhost:{GRAPH_PORT}",
662
+ limits=httpx.Limits(max_connections=1),
663
+ transport=httpx.AsyncHTTPTransport(verify=SSL),
664
+ ) as graph_client,
665
+ httpx.AsyncClient(
666
+ base_url=f"http://localhost:{REMOTE_PORT}",
667
+ limits=httpx.Limits(max_connections=1),
668
+ transport=httpx.AsyncHTTPTransport(verify=SSL),
669
+ ) as checkpointer_client,
670
+ ):
623
671
  try:
624
672
  res = await graph_client.get("/ok")
625
673
  res.raise_for_status()
626
674
  res = await checkpointer_client.get("/ok")
627
675
  res.raise_for_status()
628
- return
629
- except httpx.HTTPError:
630
- if attempt > 240:
631
- raise
632
- else:
633
- attempt += 1
634
- await asyncio.sleep(0.5)
635
-
636
-
637
- async def js_healthcheck():
638
- async with (
639
- httpx.AsyncClient(
640
- base_url=f"http://localhost:{GRAPH_PORT}",
641
- limits=httpx.Limits(max_connections=1),
642
- transport=httpx.AsyncHTTPTransport(verify=SSL),
643
- ) as graph_client,
644
- httpx.AsyncClient(
645
- base_url=f"http://localhost:{REMOTE_PORT}",
646
- limits=httpx.Limits(max_connections=1),
647
- transport=httpx.AsyncHTTPTransport(verify=SSL),
648
- ) as checkpointer_client,
649
- ):
650
- try:
651
- res = await graph_client.get("/ok")
652
- res.raise_for_status()
653
- res = await checkpointer_client.get("/ok")
654
- res.raise_for_status()
655
- return True
656
- except httpx.HTTPError as exc:
657
- logger.warning(
658
- "JS healthcheck failed. Either the JS server is not running or the event loop is blocked by a CPU-intensive task.",
659
- error=exc,
660
- )
661
- raise HTTPException(
662
- status_code=500,
663
- detail="JS healthcheck failed. Either the JS server is not running or the event loop is blocked by a CPU-intensive task.",
664
- ) from exc
676
+ return True
677
+ except httpx.HTTPError as exc:
678
+ logger.warning(
679
+ "JS healthcheck failed. Either the JS server is not running or the event loop is blocked by a CPU-intensive task.",
680
+ error=exc,
681
+ )
682
+ raise HTTPException(
683
+ status_code=500,
684
+ detail="JS healthcheck failed. Either the JS server is not running or the event loop is blocked by a CPU-intensive task.",
685
+ ) from exc
langgraph_api/stream.py CHANGED
@@ -19,15 +19,15 @@ from langgraph.errors import (
19
19
  InvalidUpdateError,
20
20
  )
21
21
  from langgraph.pregel.debug import CheckpointPayload, TaskResultPayload
22
- from langgraph.types import Command, Send
23
22
  from pydantic import ValidationError
24
23
  from pydantic.v1 import ValidationError as ValidationErrorLegacy
25
24
 
26
25
  from langgraph_api.asyncio import ValueEvent, wait_if_not_done
26
+ from langgraph_api.command import map_cmd
27
27
  from langgraph_api.graph import get_graph
28
28
  from langgraph_api.js.base import BaseRemotePregel
29
29
  from langgraph_api.metadata import HOST, PLAN, incr_nodes
30
- from langgraph_api.schema import Run, RunCommand, StreamMode
30
+ from langgraph_api.schema import Run, StreamMode
31
31
  from langgraph_api.serde import json_dumpb
32
32
  from langgraph_api.utils import AsyncConnectionProto
33
33
  from langgraph_storage.checkpoint import Checkpointer
@@ -70,32 +70,6 @@ def _preprocess_debug_checkpoint(payload: CheckpointPayload | None) -> dict[str,
70
70
  return payload
71
71
 
72
72
 
73
- def _map_cmd(cmd: RunCommand) -> Command:
74
- goto = cmd.get("goto")
75
- if goto is not None and not isinstance(goto, list):
76
- goto = [cmd.get("goto")]
77
-
78
- update = cmd.get("update")
79
- if isinstance(update, tuple | list) and all(
80
- isinstance(t, tuple | list) and len(t) == 2 and isinstance(t[0], str)
81
- for t in update
82
- ):
83
- update = [tuple(t) for t in update]
84
-
85
- return Command(
86
- update=update,
87
- goto=(
88
- [
89
- it if isinstance(it, str) else Send(it["node"], it["input"])
90
- for it in goto
91
- ]
92
- if goto
93
- else None
94
- ),
95
- resume=cmd.get("resume"),
96
- )
97
-
98
-
99
73
  async def astream_state(
100
74
  stack: AsyncExitStack,
101
75
  conn: AsyncConnectionProto,
@@ -125,7 +99,7 @@ async def astream_state(
125
99
  )
126
100
  input = kwargs.pop("input")
127
101
  if cmd := kwargs.pop("command"):
128
- input = _map_cmd(cmd)
102
+ input = map_cmd(cmd)
129
103
  stream_mode: list[StreamMode] = kwargs.pop("stream_mode")
130
104
  feedback_keys = kwargs.pop("feedback_keys", None)
131
105
  stream_modes_set: set[StreamMode] = set(stream_mode) - {"events"}
@@ -27,7 +27,18 @@ AssistantVersionChange = jsonschema_rs.validator_for(
27
27
  openapi["components"]["schemas"]["AssistantVersionChange"]
28
28
  )
29
29
  ThreadCreate = jsonschema_rs.validator_for(
30
- openapi["components"]["schemas"]["ThreadCreate"]
30
+ {
31
+ **openapi["components"]["schemas"]["ThreadCreate"],
32
+ "components": {
33
+ "schemas": {
34
+ "ThreadSuperstepUpdate": openapi["components"]["schemas"][
35
+ "ThreadSuperstepUpdate"
36
+ ],
37
+ "Command": openapi["components"]["schemas"]["Command"],
38
+ "Send": openapi["components"]["schemas"]["Send"],
39
+ }
40
+ },
41
+ }
31
42
  )
32
43
  ThreadPatch = jsonschema_rs.validator_for(
33
44
  openapi["components"]["schemas"]["ThreadPatch"]
@@ -42,6 +53,7 @@ ThreadStateUpdate = jsonschema_rs.validator_for(
42
53
  },
43
54
  }
44
55
  )
56
+
45
57
  ThreadStateCheckpointRequest = jsonschema_rs.validator_for(
46
58
  {
47
59
  **openapi["components"]["schemas"]["ThreadStateCheckpointRequest"],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langgraph-api
3
- Version: 0.0.31
3
+ Version: 0.0.32
4
4
  Summary:
5
5
  License: Elastic-2.0
6
6
  Author: Nuno Campos
@@ -16,7 +16,7 @@ Requires-Dist: jsonschema-rs (>=0.20.0,<0.30)
16
16
  Requires-Dist: langchain-core (>=0.2.38,<0.4.0)
17
17
  Requires-Dist: langgraph (>=0.2.56,<0.4.0)
18
18
  Requires-Dist: langgraph-checkpoint (>=2.0.21,<3.0)
19
- Requires-Dist: langgraph-sdk (>=0.1.57,<0.2.0)
19
+ Requires-Dist: langgraph-sdk (>=0.1.58,<0.2.0)
20
20
  Requires-Dist: langsmith (>=0.1.63,<0.4.0)
21
21
  Requires-Dist: orjson (>=3.9.7)
22
22
  Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
@@ -1,12 +1,12 @@
1
1
  LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
2
2
  langgraph_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  langgraph_api/api/__init__.py,sha256=B2kHOBhKjBRdNiWPljYuJFxP__wYPCvgtgTaWYKZGTQ,5129
4
- langgraph_api/api/assistants.py,sha256=2HEIcxaOJJ4_JQ00TKT1ehx7DMbYCGpvhShK0ZiWsSE,14043
4
+ langgraph_api/api/assistants.py,sha256=nU6tnbgdr_6Utlq0A9nw2a6xxpUM_DNuCFI42_Kcs_o,14233
5
5
  langgraph_api/api/meta.py,sha256=ifJ_Ki0Qf2DYbmY6OKlqKhLGxbt55gm0lEqH1A0cJbw,2790
6
6
  langgraph_api/api/openapi.py,sha256=f9gfmWN2AMKNUpLCpSgZuw_aeOF9jCXPdOtFT5PaTWM,10960
7
7
  langgraph_api/api/runs.py,sha256=_RWKtmjD89ALnTk56dwo2rJwEi2oghk2Tqp0l1aCcZg,16677
8
8
  langgraph_api/api/store.py,sha256=VzAJVOwO0IxosBB7km5TTf2rhlWGyPkVz_LpvbxetVY,5437
9
- langgraph_api/api/threads.py,sha256=taU61XPcCEhBPCYPZcMDsgVDwwWUWJs8p-PrXFXWY48,8661
9
+ langgraph_api/api/threads.py,sha256=meaDGF3R2bYkx0KRa_le4Ka5nOSeqlMDWtLdnEhVYSY,8930
10
10
  langgraph_api/api/ui.py,sha256=LiOZVewKOPbKEykCm30hCEaOA7vuS_Ti5hB32EEy4vw,2082
11
11
  langgraph_api/asyncio.py,sha256=ipxOGL0CuKZeHw8895ojtfoBU2fj0iJOp48uhiLAmss,7786
12
12
  langgraph_api/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -17,7 +17,8 @@ langgraph_api/auth/langsmith/client.py,sha256=eKchvAom7hdkUXauD8vHNceBDDUijrFgdT
17
17
  langgraph_api/auth/middleware.py,sha256=jU8aDSIZHdzCGdifejRF7ndHkSjBtqIHcBwFIuUdHEA,1875
18
18
  langgraph_api/auth/noop.py,sha256=Bk6Nf3p8D_iMVy_OyfPlyiJp_aEwzL-sHrbxoXpCbac,586
19
19
  langgraph_api/auth/studio_user.py,sha256=FzFQRROKDlA9JjtBuwyZvk6Mbwno5M9RVYjDO6FU3F8,186
20
- langgraph_api/cli.py,sha256=3SAwPv8EDKqJx52oD3CB3h0vN7m-LI3lT0RmYFMfkNM,11671
20
+ langgraph_api/cli.py,sha256=zzlGrMgiRXITuvztQ12y26ljOgEXk93Vq2quKYhbaD8,11750
21
+ langgraph_api/command.py,sha256=3O9v3i0OPa96ARyJ_oJbLXkfO8rPgDhLCswgO9koTFA,768
21
22
  langgraph_api/config.py,sha256=XMJEVfPe1LrmVKGM5sisQs1eZOUDyU3JwscwJOm0q_k,8572
22
23
  langgraph_api/cron_scheduler.py,sha256=9yzbbGxzNgJdIg4ZT7yu2oTwT_wRuPxD1c2sbbd52xs,2630
23
24
  langgraph_api/errors.py,sha256=Bu_i5drgNTyJcLiyrwVE_6-XrSU50BHf9TDpttki9wQ,1690
@@ -30,7 +31,7 @@ langgraph_api/js/client.mts,sha256=2dptAX8fMowV9OC4DU4khjpZUgALBLVBTu3jTQbeUJY,2
30
31
  langgraph_api/js/errors.py,sha256=Cm1TKWlUCwZReDC5AQ6SgNIVGD27Qov2xcgHyf8-GXo,361
31
32
  langgraph_api/js/global.d.ts,sha256=cLJRZfYVGmgQ6o_xFevVNNTIi918ZUdxVRnpLVSjiAY,133
32
33
  langgraph_api/js/package.json,sha256=j6DMoVgwRqWqTwdd7R1f-kvmiTUAbO3HaUhM8K64lbE,1224
33
- langgraph_api/js/remote.py,sha256=jUUgdskUbDR_tuhZFrJo4OewbndQ76joczBv-mZHG9k,22637
34
+ langgraph_api/js/remote.py,sha256=g0H2x3W7kejxswhgZfFaljzl4Y7agMCnO5BdNVj1rDY,23474
34
35
  langgraph_api/js/schema.py,sha256=7idnv7URlYUdSNMBXQcw7E4SxaPxCq_Oxwnlml8q5ik,408
35
36
  langgraph_api/js/src/graph.mts,sha256=mRyMUp03Fwd5DlmNIFl3RiUCQuJ5XwmFp1AfAeKDfVc,3169
36
37
  langgraph_api/js/src/hooks.mjs,sha256=XtktgmIHlls_DsknAuwib-z7TqCm0haRoTXvnkgzMuo,601
@@ -74,9 +75,9 @@ langgraph_api/serde.py,sha256=VoJ7Z1IuqrQGXFzEP1qijAITtWCrmjtVqlCRuScjXJI,3533
74
75
  langgraph_api/server.py,sha256=CiNK327zTsEpoVGeJK1JOtZHvOBYRoz0CnBTZUmsC7c,4567
75
76
  langgraph_api/sse.py,sha256=2wNodCOP2eg7a9mpSu0S3FQ0CHk2BBV_vv0UtIgJIcc,4034
76
77
  langgraph_api/state.py,sha256=8jx4IoTCOjTJuwzuXJKKFwo1VseHjNnw_CCq4x1SW14,2284
77
- langgraph_api/stream.py,sha256=LKXxol0y9xEsrgnbsDwyeKIPBDE-75uVz01OcLI-_QE,11976
78
+ langgraph_api/stream.py,sha256=AgDjVxE8Tf3_TAB5td36mVy7_-9OwKcZJpHRWJdVCSM,11281
78
79
  langgraph_api/utils.py,sha256=92mSti9GfGdMRRWyESKQW5yV-75Z9icGHnIrBYvdypU,3619
79
- langgraph_api/validation.py,sha256=McizHlz-Ez8Jhdbc79mbPSde7GIuf2Jlbjx2yv_l6dA,4475
80
+ langgraph_api/validation.py,sha256=LnEdfgID2Z0HP75R_hqWxCn5d5ULGcDJ3r1xaDVv6-w,4845
80
81
  langgraph_api/webhook.py,sha256=1ncwO0rIZcj-Df9sxSnFEzd1gP1bfS4okeZQS8NSRoE,1382
81
82
  langgraph_api/worker.py,sha256=7yQfZBANt1kgJDOEs5B5c3Xy65lzNMmngVbBqLs-r5s,9802
82
83
  langgraph_license/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -86,15 +87,15 @@ langgraph_storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
86
87
  langgraph_storage/checkpoint.py,sha256=V4t2GwYEJdPCHbhq_4Udhlv0TWKDzlMu_rlNPdTDc50,3589
87
88
  langgraph_storage/database.py,sha256=I0AgFeJ-NSTT34vxKxQBUf1z2syFP0S8QpKCqTixrzY,5652
88
89
  langgraph_storage/inmem_stream.py,sha256=8bxkILIuFpr7P7RQ37SQAxrpRKvmbHdRB_nbfFiomlk,3263
89
- langgraph_storage/ops.py,sha256=6qj3zh3T7KZco_vz9FW1cI82sbrefo6u2BRmrUDXvZI,69529
90
+ langgraph_storage/ops.py,sha256=0DhtDQllBnV495DPrwhbR_bQUg7ZrGBe0FxSZcicz5g,72469
90
91
  langgraph_storage/queue.py,sha256=UDgsUTtUMfBSRDrQ8Onis-FJO4n7KTsX6sdpbY8Hs0A,5055
91
92
  langgraph_storage/retry.py,sha256=XmldOP4e_H5s264CagJRVnQMDFcEJR_dldVR1Hm5XvM,763
92
93
  langgraph_storage/store.py,sha256=33-J5-Xvobb9ArSa-GezP5KtfXgzWkHUHPyjRYmdw-E,2985
93
94
  langgraph_storage/ttl_dict.py,sha256=FlpEY8EANeXWKo_G5nmIotPquABZGyIJyk6HD9u6vqY,1533
94
95
  logging.json,sha256=3RNjSADZmDq38eHePMm1CbP6qZ71AmpBtLwCmKU9Zgo,379
95
- openapi.json,sha256=8NhoZn3Dse6Ia0i7fXCB46sdGnEg5qAhov_8of-6m0o,125282
96
- langgraph_api-0.0.31.dist-info/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
97
- langgraph_api-0.0.31.dist-info/METADATA,sha256=lSR24lckxz9BgmrjjqG7b04Q22m5m1W49dHtggG6BeY,4027
98
- langgraph_api-0.0.31.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
99
- langgraph_api-0.0.31.dist-info/entry_points.txt,sha256=3EYLgj89DfzqJHHYGxPH4A_fEtClvlRbWRUHaXO7hj4,77
100
- langgraph_api-0.0.31.dist-info/RECORD,,
96
+ openapi.json,sha256=6okfuwPAKB2Hi1tup-EFza49pmN_iD2Yt5JktMALcq4,127984
97
+ langgraph_api-0.0.32.dist-info/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
98
+ langgraph_api-0.0.32.dist-info/METADATA,sha256=LrWNwNTUVge4rKtDDFwJyFKdE5WRTPCLPj8MjSHsDvc,4027
99
+ langgraph_api-0.0.32.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
100
+ langgraph_api-0.0.32.dist-info/entry_points.txt,sha256=3EYLgj89DfzqJHHYGxPH4A_fEtClvlRbWRUHaXO7hj4,77
101
+ langgraph_api-0.0.32.dist-info/RECORD,,
langgraph_storage/ops.py CHANGED
@@ -22,6 +22,7 @@ from starlette.exceptions import HTTPException
22
22
 
23
23
  from langgraph_api.asyncio import SimpleTaskGroup, ValueEvent, create_task
24
24
  from langgraph_api.auth.custom import handle_event
25
+ from langgraph_api.command import map_cmd
25
26
  from langgraph_api.errors import UserInterrupt, UserRollback
26
27
  from langgraph_api.graph import get_graph
27
28
  from langgraph_api.schema import (
@@ -1081,6 +1082,78 @@ class Threads(Authenticated):
1081
1082
  else:
1082
1083
  raise HTTPException(status_code=400, detail="Thread has no graph ID.")
1083
1084
 
1085
+ @staticmethod
1086
+ async def bulk(
1087
+ conn: InMemConnectionProto,
1088
+ *,
1089
+ config: Config,
1090
+ supersteps: Sequence[dict],
1091
+ ctx: Auth.types.BaseAuthContext | None = None,
1092
+ ) -> ThreadUpdateResponse:
1093
+ """Update a thread with a batch of state updates."""
1094
+
1095
+ from langgraph.pregel.types import StateUpdate
1096
+
1097
+ thread_id = _ensure_uuid(config["configurable"]["thread_id"])
1098
+ filters = await Threads.handle_event(
1099
+ ctx,
1100
+ "update",
1101
+ Auth.types.ThreadsUpdate(thread_id=thread_id),
1102
+ )
1103
+
1104
+ thread_iter = await Threads.get(conn, thread_id, ctx=ctx)
1105
+ thread = await fetchone(
1106
+ thread_iter, not_found_detail=f"Thread {thread_id} not found."
1107
+ )
1108
+
1109
+ thread_config = thread["config"]
1110
+ metadata = thread["metadata"]
1111
+
1112
+ if not thread:
1113
+ raise HTTPException(status_code=404, detail="Thread not found")
1114
+
1115
+ if not _check_filter_match(metadata, filters):
1116
+ raise HTTPException(status_code=403, detail="Forbidden")
1117
+
1118
+ if graph_id := metadata.get("graph_id"):
1119
+ config["configurable"].setdefault("graph_id", graph_id)
1120
+ config["configurable"].setdefault("checkpoint_ns", "")
1121
+
1122
+ async with get_graph(
1123
+ graph_id, thread_config, checkpointer=Checkpointer(conn)
1124
+ ) as graph:
1125
+ next_config = await graph.abulk_update_state(
1126
+ config,
1127
+ [
1128
+ [
1129
+ StateUpdate(
1130
+ map_cmd(update.get("command"))
1131
+ if update.get("command")
1132
+ else update.get("values"),
1133
+ update.get("as_node"),
1134
+ )
1135
+ for update in superstep.get("updates", [])
1136
+ ]
1137
+ for superstep in supersteps
1138
+ ],
1139
+ )
1140
+
1141
+ state = await Threads.State.get(
1142
+ conn, config, subgraphs=False, ctx=ctx
1143
+ )
1144
+
1145
+ # update thread values
1146
+ for thread in conn.store["threads"]:
1147
+ if thread["thread_id"] == thread_id:
1148
+ thread["values"] = state.values
1149
+ break
1150
+
1151
+ return ThreadUpdateResponse(
1152
+ checkpoint=next_config["configurable"],
1153
+ )
1154
+ else:
1155
+ raise HTTPException(status_code=400, detail="Thread has no graph ID")
1156
+
1084
1157
  @staticmethod
1085
1158
  async def list(
1086
1159
  conn: InMemConnectionProto,
@@ -1905,7 +1978,8 @@ class Crons:
1905
1978
  conn: InMemConnectionProto,
1906
1979
  ctx: Auth.types.BaseAuthContext | None = None,
1907
1980
  ) -> AsyncIterator[Cron]:
1908
- raise NotImplementedError
1981
+ yield
1982
+ raise NotImplementedError("The in-mem server does not implement Crons.")
1909
1983
 
1910
1984
  @staticmethod
1911
1985
  async def set_next_run_date(
openapi.json CHANGED
@@ -805,6 +805,58 @@
805
805
  }
806
806
  }
807
807
  },
808
+ "/threads/state/bulk": {
809
+ "post": {
810
+ "tags": [
811
+ "Threads"
812
+ ],
813
+ "summary": "Bulk Update Thread State",
814
+ "description": "Create a new thread from a batch of state updates.",
815
+ "operationId": "bulk_update_thread_state_post",
816
+ "requestBody": {
817
+ "content": {
818
+ "application/json": {
819
+ "schema": {
820
+ "$ref": "#/components/schemas/ThreadStateBulkUpdate"
821
+ }
822
+ }
823
+ },
824
+ "required": true
825
+ },
826
+ "responses": {
827
+ "200": {
828
+ "description": "Success",
829
+ "content": {
830
+ "application/json": {
831
+ "schema": {
832
+ "$ref": "#/components/schemas/Thread"
833
+ }
834
+ }
835
+ }
836
+ },
837
+ "409": {
838
+ "description": "Conflict",
839
+ "content": {
840
+ "application/json": {
841
+ "schema": {
842
+ "$ref": "#/components/schemas/ErrorResponse"
843
+ }
844
+ }
845
+ }
846
+ },
847
+ "422": {
848
+ "description": "Validation Error",
849
+ "content": {
850
+ "application/json": {
851
+ "schema": {
852
+ "$ref": "#/components/schemas/ErrorResponse"
853
+ }
854
+ }
855
+ }
856
+ }
857
+ }
858
+ }
859
+ },
808
860
  "/threads/{thread_id}/state": {
809
861
  "get": {
810
862
  "tags": [
@@ -3906,6 +3958,19 @@
3906
3958
  "title": "If Exists",
3907
3959
  "description": "How to handle duplicate creation. Must be either 'raise' (raise error if duplicate), or 'do_nothing' (return existing thread).",
3908
3960
  "default": "raise"
3961
+ },
3962
+ "supersteps": {
3963
+ "type": "array",
3964
+ "items": {
3965
+ "type": "object",
3966
+ "properties": {
3967
+ "updates": {
3968
+ "type": "array",
3969
+ "items": { "$ref": "#/components/schemas/ThreadSuperstepUpdate" }
3970
+ }
3971
+ },
3972
+ "required": ["updates"]
3973
+ }
3909
3974
  }
3910
3975
  },
3911
3976
  "type": "object",
@@ -4094,6 +4159,43 @@
4094
4159
  "title": "ThreadStateUpdate",
4095
4160
  "description": "Payload for updating the state of a thread."
4096
4161
  },
4162
+ "ThreadSuperstepUpdate": {
4163
+ "properties": {
4164
+ "values": {
4165
+ "anyOf": [
4166
+ {
4167
+ "type": "array",
4168
+ "items": {
4169
+ "type": "object"
4170
+ }
4171
+ },
4172
+ {
4173
+ "type": "object"
4174
+ },
4175
+ {
4176
+ "type": "null"
4177
+ }
4178
+ ]
4179
+ },
4180
+ "command": {
4181
+ "anyOf": [
4182
+ {
4183
+ "$ref": "#/components/schemas/Command"
4184
+ },
4185
+ {
4186
+ "type": "null"
4187
+ }
4188
+ ],
4189
+ "description": "The command associated with the update."
4190
+ },
4191
+ "as_node": {
4192
+ "type": "string",
4193
+ "description": "Update the state as if this node had just executed."
4194
+ }
4195
+ },
4196
+ "required": ["as_node"],
4197
+ "type": "object"
4198
+ },
4097
4199
  "ThreadStateUpdateResponse": {
4098
4200
  "properties": {
4099
4201
  "checkpoint": {