langgraph-api 0.0.31__tar.gz → 0.0.32__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (99) hide show
  1. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/PKG-INFO +2 -2
  2. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/api/assistants.py +7 -1
  3. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/api/threads.py +10 -1
  4. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/cli.py +8 -7
  5. langgraph_api-0.0.32/langgraph_api/command.py +29 -0
  6. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/remote.py +72 -51
  7. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/stream.py +3 -29
  8. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/validation.py +13 -1
  9. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_storage/ops.py +75 -1
  10. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/openapi.json +102 -0
  11. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/pyproject.toml +3 -3
  12. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/LICENSE +0 -0
  13. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/README.md +0 -0
  14. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/__init__.py +0 -0
  15. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/api/__init__.py +0 -0
  16. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/api/meta.py +0 -0
  17. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/api/openapi.py +0 -0
  18. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/api/runs.py +0 -0
  19. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/api/store.py +0 -0
  20. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/api/ui.py +0 -0
  21. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/asyncio.py +0 -0
  22. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/auth/__init__.py +0 -0
  23. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/auth/custom.py +0 -0
  24. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/auth/langsmith/__init__.py +0 -0
  25. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/auth/langsmith/backend.py +0 -0
  26. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/auth/langsmith/client.py +0 -0
  27. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/auth/middleware.py +0 -0
  28. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/auth/noop.py +0 -0
  29. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/auth/studio_user.py +0 -0
  30. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/config.py +0 -0
  31. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/cron_scheduler.py +0 -0
  32. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/errors.py +0 -0
  33. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/graph.py +0 -0
  34. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/http.py +0 -0
  35. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/.gitignore +0 -0
  36. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/base.py +0 -0
  37. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/build.mts +0 -0
  38. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/client.mts +0 -0
  39. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/errors.py +0 -0
  40. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/global.d.ts +0 -0
  41. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/package.json +0 -0
  42. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/schema.py +0 -0
  43. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/src/graph.mts +0 -0
  44. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/src/hooks.mjs +0 -0
  45. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/src/parser/parser.mts +0 -0
  46. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/src/parser/parser.worker.mjs +0 -0
  47. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/src/schema/types.mts +0 -0
  48. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/src/schema/types.template.mts +0 -0
  49. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/src/utils/importMap.mts +0 -0
  50. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
  51. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/src/utils/serde.mts +0 -0
  52. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/sse.py +0 -0
  53. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/tests/api.test.mts +0 -0
  54. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/tests/compose-postgres.yml +0 -0
  55. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/tests/graphs/.gitignore +0 -0
  56. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/tests/graphs/agent.css +0 -0
  57. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/tests/graphs/agent.mts +0 -0
  58. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/tests/graphs/agent.ui.tsx +0 -0
  59. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/tests/graphs/delay.mts +0 -0
  60. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/tests/graphs/error.mts +0 -0
  61. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/tests/graphs/langgraph.json +0 -0
  62. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/tests/graphs/nested.mts +0 -0
  63. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/tests/graphs/package.json +0 -0
  64. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/tests/graphs/weather.mts +0 -0
  65. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/tests/graphs/yarn.lock +0 -0
  66. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/tests/parser.test.mts +0 -0
  67. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/tests/utils.mts +0 -0
  68. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/js/yarn.lock +0 -0
  69. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/lifespan.py +0 -0
  70. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/logging.py +0 -0
  71. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/metadata.py +0 -0
  72. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/middleware/__init__.py +0 -0
  73. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/middleware/http_logger.py +0 -0
  74. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/middleware/private_network.py +0 -0
  75. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/models/__init__.py +0 -0
  76. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/models/run.py +0 -0
  77. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/patch.py +0 -0
  78. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/queue_entrypoint.py +0 -0
  79. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/route.py +0 -0
  80. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/schema.py +0 -0
  81. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/serde.py +0 -0
  82. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/server.py +0 -0
  83. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/sse.py +0 -0
  84. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/state.py +0 -0
  85. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/utils.py +0 -0
  86. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/webhook.py +0 -0
  87. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_api/worker.py +0 -0
  88. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_license/__init__.py +0 -0
  89. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_license/middleware.py +0 -0
  90. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_license/validation.py +0 -0
  91. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_storage/__init__.py +0 -0
  92. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_storage/checkpoint.py +0 -0
  93. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_storage/database.py +0 -0
  94. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_storage/inmem_stream.py +0 -0
  95. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_storage/queue.py +0 -0
  96. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_storage/retry.py +0 -0
  97. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_storage/store.py +0 -0
  98. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/langgraph_storage/ttl_dict.py +0 -0
  99. {langgraph_api-0.0.31 → langgraph_api-0.0.32}/logging.json +0 -0
@@ -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)
@@ -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
 
@@ -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
@@ -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"],
@@ -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(
@@ -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": {
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "langgraph-api"
3
- version = "0.0.31"
3
+ version = "0.0.32"
4
4
  description = ""
5
5
  authors = [
6
6
  "Nuno Campos <nuno@langchain.dev>",
@@ -35,7 +35,7 @@ jsonschema-rs = ">=0.20.0,<0.30"
35
35
  structlog = ">=24.1.0,<26"
36
36
  pyjwt = "^2.9.0"
37
37
  cryptography = "^43.0.3"
38
- langgraph-sdk = "^0.1.57"
38
+ langgraph-sdk = "^0.1.58"
39
39
 
40
40
  [tool.poetry.group.dev.dependencies]
41
41
  ruff = "^0.6.2"
@@ -48,7 +48,7 @@ pytest-repeat = "^0.9.3"
48
48
  pytest-retry = "^1.6.3"
49
49
  pytest-httpserver = "^1.1.0"
50
50
  fastapi = "^0.115.8"
51
- langgraph = ">=0.3.9"
51
+ langgraph = ">=0.3.17"
52
52
  pycryptodome = "^3.22.0"
53
53
 
54
54
  [tool.pytest.ini_options]
File without changes
File without changes