langgraph-api 0.0.45__tar.gz → 0.0.47__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 (105) hide show
  1. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/PKG-INFO +3 -3
  2. langgraph_api-0.0.47/langgraph_api/__init__.py +1 -0
  3. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/ui.py +19 -14
  4. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/cli.py +9 -0
  5. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/config.py +3 -0
  6. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/graph.py +80 -22
  7. langgraph_api-0.0.47/langgraph_api/js/build.mts +68 -0
  8. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/client.mts +35 -3
  9. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/package.json +2 -1
  10. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/remote.py +9 -3
  11. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/api.test.mts +169 -76
  12. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/compose-postgres.yml +2 -1
  13. langgraph_api-0.0.47/langgraph_api/js/tests/graphs/command.mts +48 -0
  14. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/langgraph.json +2 -1
  15. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/package.json +3 -0
  16. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/yarn.lock +5 -0
  17. langgraph_api-0.0.47/langgraph_api/js/ui.py +93 -0
  18. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/yarn.lock +26 -6
  19. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/lifespan.py +3 -0
  20. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/patch.py +4 -0
  21. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/openapi.json +8 -1
  22. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/pyproject.toml +3 -3
  23. langgraph_api-0.0.45/langgraph_api/__init__.py +0 -1
  24. langgraph_api-0.0.45/langgraph_api/js/build.mts +0 -105
  25. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/LICENSE +0 -0
  26. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/README.md +0 -0
  27. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/__init__.py +0 -0
  28. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/assistants.py +0 -0
  29. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/mcp.py +0 -0
  30. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/meta.py +0 -0
  31. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/openapi.py +0 -0
  32. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/runs.py +0 -0
  33. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/store.py +0 -0
  34. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/threads.py +0 -0
  35. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/asyncio.py +0 -0
  36. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/auth/__init__.py +0 -0
  37. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/auth/custom.py +0 -0
  38. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/auth/langsmith/__init__.py +0 -0
  39. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/auth/langsmith/backend.py +0 -0
  40. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/auth/langsmith/client.py +0 -0
  41. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/auth/middleware.py +0 -0
  42. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/auth/noop.py +0 -0
  43. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/auth/studio_user.py +0 -0
  44. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/command.py +0 -0
  45. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/cron_scheduler.py +0 -0
  46. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/errors.py +0 -0
  47. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/http.py +0 -0
  48. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/.gitignore +0 -0
  49. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/base.py +0 -0
  50. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/errors.py +0 -0
  51. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/global.d.ts +0 -0
  52. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/schema.py +0 -0
  53. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/graph.mts +0 -0
  54. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/hooks.mjs +0 -0
  55. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/parser/parser.mts +0 -0
  56. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/parser/parser.worker.mjs +0 -0
  57. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/schema/types.mts +0 -0
  58. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/schema/types.template.mts +0 -0
  59. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/utils/importMap.mts +0 -0
  60. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
  61. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/utils/serde.mts +0 -0
  62. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/sse.py +0 -0
  63. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/.gitignore +0 -0
  64. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/agent.css +0 -0
  65. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/agent.mts +0 -0
  66. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/agent.ui.tsx +0 -0
  67. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/delay.mts +0 -0
  68. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/dynamic.mts +0 -0
  69. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/error.mts +0 -0
  70. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/nested.mts +0 -0
  71. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/weather.mts +0 -0
  72. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/parser.test.mts +0 -0
  73. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/utils.mts +0 -0
  74. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/logging.py +0 -0
  75. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/metadata.py +0 -0
  76. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/middleware/__init__.py +0 -0
  77. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/middleware/http_logger.py +0 -0
  78. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/middleware/private_network.py +0 -0
  79. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/models/__init__.py +0 -0
  80. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/models/run.py +0 -0
  81. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/queue_entrypoint.py +0 -0
  82. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/route.py +0 -0
  83. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/schema.py +0 -0
  84. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/serde.py +0 -0
  85. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/server.py +0 -0
  86. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/sse.py +0 -0
  87. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/state.py +0 -0
  88. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/stream.py +0 -0
  89. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/thread_ttl.py +0 -0
  90. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/utils.py +0 -0
  91. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/validation.py +0 -0
  92. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/webhook.py +0 -0
  93. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/worker.py +0 -0
  94. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_license/__init__.py +0 -0
  95. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_license/middleware.py +0 -0
  96. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_license/validation.py +0 -0
  97. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_storage/__init__.py +0 -0
  98. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_storage/checkpoint.py +0 -0
  99. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_storage/database.py +0 -0
  100. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_storage/inmem_stream.py +0 -0
  101. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_storage/ops.py +0 -0
  102. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_storage/queue.py +0 -0
  103. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_storage/retry.py +0 -0
  104. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_storage/store.py +0 -0
  105. {langgraph_api-0.0.45 → langgraph_api-0.0.47}/logging.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: langgraph-api
3
- Version: 0.0.45
3
+ Version: 0.0.47
4
4
  Summary:
5
5
  License: Elastic-2.0
6
6
  Author: Nuno Campos
@@ -13,13 +13,13 @@ Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Requires-Dist: blockbuster (>=1.5.24,<2.0.0)
15
15
  Requires-Dist: cloudpickle (>=3.0.0,<4.0.0)
16
- Requires-Dist: cryptography (>=43.0.3,<44.0.0)
16
+ Requires-Dist: cryptography (>=42.0.0,<45.0)
17
17
  Requires-Dist: httpx (>=0.25.0)
18
18
  Requires-Dist: jsonschema-rs (>=0.20.0,<0.30)
19
19
  Requires-Dist: langchain-core (>=0.2.38,<0.4.0)
20
20
  Requires-Dist: langgraph (>=0.2.56,<0.4.0)
21
21
  Requires-Dist: langgraph-checkpoint (>=2.0.23,<3.0)
22
- Requires-Dist: langgraph-sdk (>=0.1.59,<0.2.0)
22
+ Requires-Dist: langgraph-sdk (>=0.1.61,<0.2.0)
23
23
  Requires-Dist: langsmith (>=0.1.63,<0.4.0)
24
24
  Requires-Dist: orjson (>=3.9.7)
25
25
  Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
@@ -0,0 +1 @@
1
+ __version__ = "0.0.47"
@@ -1,34 +1,39 @@
1
1
  import json
2
2
  import os
3
- from functools import lru_cache
4
- from pathlib import Path
5
3
  from typing import TypedDict
6
4
 
5
+ from anyio import open_file
7
6
  from orjson import loads
8
7
  from starlette.responses import Response
9
8
  from starlette.routing import BaseRoute, Mount
10
9
  from starlette.staticfiles import StaticFiles
11
10
 
11
+ from langgraph_api.js.ui import UI_PUBLIC_DIR, UI_SCHEMAS_FILE
12
12
  from langgraph_api.route import ApiRequest, ApiRoute
13
13
 
14
- # Get path to built UI assets
15
- UI_DIR = Path(os.path.dirname(__file__)).parent / "js" / "ui"
16
- SCHEMAS_FILE = Path(os.path.dirname(__file__)).parent / "js" / "client.ui.schemas.json"
17
-
18
14
 
19
15
  class UiSchema(TypedDict):
20
16
  name: str
21
17
  assets: list[str]
22
18
 
23
19
 
24
- @lru_cache(maxsize=1)
25
- def load_ui_schemas() -> dict[str, UiSchema]:
20
+ _UI_SCHEMAS_CACHE: dict[str, UiSchema] | None = None
21
+
22
+
23
+ async def load_ui_schemas() -> dict[str, UiSchema]:
26
24
  """Load and cache UI schema mappings from JSON file."""
27
- if not SCHEMAS_FILE.exists():
28
- return {}
25
+ global _UI_SCHEMAS_CACHE
26
+
27
+ if _UI_SCHEMAS_CACHE is not None:
28
+ return _UI_SCHEMAS_CACHE
29
+
30
+ if not UI_SCHEMAS_FILE.exists():
31
+ _UI_SCHEMAS_CACHE = {}
32
+ else:
33
+ async with await open_file(UI_SCHEMAS_FILE, mode="r") as f:
34
+ _UI_SCHEMAS_CACHE = loads(await f.read())
29
35
 
30
- with open(SCHEMAS_FILE) as f:
31
- return loads(f.read())
36
+ return _UI_SCHEMAS_CACHE
32
37
 
33
38
 
34
39
  async def handle_ui(request: ApiRequest) -> Response:
@@ -38,7 +43,7 @@ async def handle_ui(request: ApiRequest) -> Response:
38
43
  message = await request.json(schema=None)
39
44
 
40
45
  # Load UI file paths from schema
41
- schemas = load_ui_schemas()
46
+ schemas = await load_ui_schemas()
42
47
 
43
48
  if graph_id not in schemas:
44
49
  return Response(f"UI not found for graph '{graph_id}'", status_code=404)
@@ -64,5 +69,5 @@ async def handle_ui(request: ApiRequest) -> Response:
64
69
 
65
70
  ui_routes: list[BaseRoute] = [
66
71
  ApiRoute("/ui/{graph_id}", handle_ui, methods=["POST"]),
67
- Mount("/ui", StaticFiles(directory=UI_DIR, check_dir=False)),
72
+ Mount("/ui", StaticFiles(directory=UI_PUBLIC_DIR, check_dir=False)),
68
73
  ]
@@ -129,6 +129,8 @@ def run_server(
129
129
  store: typing.Optional["StoreConfig"] = None,
130
130
  auth: AuthConfig | None = None,
131
131
  http: typing.Optional["HttpConfig"] = None,
132
+ ui: dict | None = None,
133
+ ui_config: dict | None = None,
132
134
  studio_url: str | None = None,
133
135
  disable_persistence: bool = False,
134
136
  allow_blocking: bool = False,
@@ -190,6 +192,9 @@ def run_server(
190
192
  LANGSMITH_LANGGRAPH_API_VARIANT="local_dev",
191
193
  LANGGRAPH_AUTH=json.dumps(auth) if auth else None,
192
194
  LANGGRAPH_HTTP=json.dumps(http) if http else None,
195
+ LANGGRAPH_UI=json.dumps(ui) if ui else None,
196
+ LANGGRAPH_UI_CONFIG=json.dumps(ui_config) if ui_config else None,
197
+ LANGGRAPH_UI_BUNDLER="true",
193
198
  LANGGRAPH_API_URL=local_url,
194
199
  LANGGRAPH_DISABLE_FILE_PERSISTENCE=str(disable_persistence).lower(),
195
200
  # If true, we will not raise on blocking IO calls (via blockbuster)
@@ -349,6 +354,8 @@ def main():
349
354
 
350
355
  graphs = config_data.get("graphs", {})
351
356
  auth = config_data.get("auth")
357
+ ui = config_data.get("ui")
358
+ ui_config = config_data.get("ui_config")
352
359
  run_server(
353
360
  args.host,
354
361
  args.port,
@@ -360,6 +367,8 @@ def main():
360
367
  wait_for_client=args.wait_for_client,
361
368
  env=config_data.get("env", None),
362
369
  auth=auth,
370
+ ui=ui,
371
+ ui_config=ui_config,
363
372
  )
364
373
 
365
374
 
@@ -325,3 +325,6 @@ USES_INDEXING = (
325
325
  USES_CUSTOM_APP = HTTP_CONFIG and HTTP_CONFIG.get("app")
326
326
 
327
327
  API_VARIANT = env("LANGSMITH_LANGGRAPH_API_VARIANT", cast=str, default="")
328
+
329
+ # UI
330
+ UI_USE_BUNDLER = env("LANGGRAPH_UI_BUNDLER", cast=bool, default=False)
@@ -42,7 +42,13 @@ NAMESPACE_GRAPH = UUID("6ba7b821-9dad-11d1-80b4-00c04fd430c8")
42
42
  FACTORY_ACCEPTS_CONFIG: dict[str, bool] = {}
43
43
 
44
44
 
45
- async def register_graph(graph_id: str, graph: GraphValue, config: dict | None) -> None:
45
+ async def register_graph(
46
+ graph_id: str,
47
+ graph: GraphValue,
48
+ config: dict | None,
49
+ *,
50
+ description: str | None = None,
51
+ ) -> None:
46
52
  """Register a graph."""
47
53
  from langgraph_storage.database import connect
48
54
  from langgraph_storage.ops import Assistants
@@ -60,6 +66,7 @@ async def register_graph(graph_id: str, graph: GraphValue, config: dict | None)
60
66
  config=config or {},
61
67
  if_exists="do_nothing",
62
68
  name=graph_id,
69
+ description=description,
63
70
  )
64
71
 
65
72
 
@@ -149,13 +156,26 @@ def get_assistant_id(assistant_id: str) -> str:
149
156
 
150
157
 
151
158
  class GraphSpec(NamedTuple):
152
- """A graph specification."""
159
+ """A graph specification.
160
+
161
+ This is a definition of the graph that can be used to load the graph
162
+ from a file or module.
163
+ """
153
164
 
154
165
  id: str
166
+ """The ID of the graph."""
155
167
  path: str | None = None
156
168
  module: str | None = None
157
169
  variable: str | None = None
158
170
  config: dict | None = None
171
+ """The configuration for the graph.
172
+
173
+ Contains information such as: tags, recursion_limit and configurable.
174
+
175
+ Configurable is a dict containing user defined values for the graph.
176
+ """
177
+ description: str | None = None
178
+ """A description of the graph"""
159
179
 
160
180
 
161
181
  js_bg_tasks: set[asyncio.Task] = set()
@@ -193,9 +213,33 @@ async def collect_graphs_from_env(register: bool = False) -> None:
193
213
 
194
214
  if paths_str:
195
215
  specs = []
196
- for key, value in json.loads(paths_str).items():
216
+ # graphs-config can be either a mapping from graph id to path where the graph
217
+ # is defined or graph id to a dictionary containing information about the graph.
218
+ graphs_config = json.loads(paths_str)
219
+
220
+ for key, value in graphs_config.items():
221
+ if isinstance(value, dict) and "path" in value:
222
+ source = value["path"]
223
+ elif isinstance(value, str):
224
+ source = value
225
+ else:
226
+ msg = (
227
+ f"Invalid value '{value}' for graph '{key}'. "
228
+ "Expected a string or a dictionary. "
229
+ "If a string, it should be the path to the graph definition. "
230
+ "For example: '/path/to/graph.py:graph_variable' "
231
+ "or 'my.module:graph_variable'. "
232
+ "If a dictionary, then it needs to contains a `path` key with the "
233
+ "path to the graph definition."
234
+ "It can also contains additional configuration for the graph; "
235
+ "e.g., `description`."
236
+ "For example: {'path': '/path/to/graph.py:graph_variable', "
237
+ "'description': 'My graph'}"
238
+ )
239
+ raise TypeError(msg)
240
+
197
241
  try:
198
- path_or_module, variable = value.rsplit(":", maxsplit=1)
242
+ path_or_module, variable = source.rsplit(":", maxsplit=1)
199
243
  except ValueError as e:
200
244
  raise ValueError(
201
245
  f"Invalid path '{value}' for graph '{key}'."
@@ -203,22 +247,30 @@ async def collect_graphs_from_env(register: bool = False) -> None:
203
247
  " Expected one of the following formats:"
204
248
  " 'my.module:variable_name' or '/path/to/file.py:variable_name'"
205
249
  ) from e
250
+
251
+ graph_config = config_per_graph.get(key, {})
252
+ description = (
253
+ value.get("description", None) if isinstance(value, dict) else None
254
+ )
255
+
256
+ # Module syntax uses `.` instead of `/` to separate directories
257
+ if "/" in path_or_module:
258
+ path = path_or_module
259
+ module_ = None
260
+ else:
261
+ path = None
262
+ module_ = path_or_module
263
+
206
264
  specs.append(
207
265
  GraphSpec(
208
266
  key,
209
- module=path_or_module,
210
- variable=variable,
211
- config=config_per_graph.get(key),
212
- )
213
- if "/" not in value
214
- else GraphSpec(
215
- key,
216
- path=path_or_module,
267
+ module=module_,
268
+ path=path,
217
269
  variable=variable,
218
- config=config_per_graph.get(key),
270
+ config=graph_config,
271
+ description=description,
219
272
  )
220
273
  )
221
-
222
274
  else:
223
275
  specs = [
224
276
  GraphSpec(
@@ -270,12 +322,16 @@ async def collect_graphs_from_env(register: bool = False) -> None:
270
322
  for spec in js_specs:
271
323
  graph = RemotePregel(graph_id=spec.id)
272
324
  if register:
273
- await register_graph(spec.id, graph, spec.config)
325
+ await register_graph(
326
+ spec.id, graph, spec.config, description=spec.description
327
+ )
274
328
 
275
329
  for spec in py_specs:
276
330
  graph = await run_in_executor(None, _graph_from_spec, spec)
277
331
  if register:
278
- await register_graph(spec.id, graph, spec.config)
332
+ await register_graph(
333
+ spec.id, graph, spec.config, description=spec.description
334
+ )
279
335
 
280
336
 
281
337
  def _handle_exception(task: asyncio.Task) -> None:
@@ -289,7 +345,7 @@ def _handle_exception(task: asyncio.Task) -> None:
289
345
 
290
346
 
291
347
  async def stop_remote_graphs() -> None:
292
- logger.info("Cancelling remote graphs")
348
+ logger.info("Shutting down remote graphs")
293
349
  for task in js_bg_tasks:
294
350
  task.cancel("Stopping remote graphs.")
295
351
 
@@ -375,14 +431,16 @@ def _graph_from_spec(spec: GraphSpec) -> GraphValue:
375
431
  # We don't want to fail real deployments, but this will help folks catch unnecessary custom components
376
432
  # before they deploy
377
433
  if config.API_VARIANT == "local_dev":
378
- has_checkpointer = graph.checkpointer is not None
379
- has_store = graph.store is not None
434
+ has_checkpointer = isinstance(graph.checkpointer, BaseCheckpointSaver)
435
+ has_store = isinstance(graph.store, BaseStore)
380
436
  if has_checkpointer or has_store:
381
437
  components = []
382
438
  if has_checkpointer:
383
- components.append("checkpointer")
439
+ components.append(
440
+ f"checkpointer (type {type(graph.checkpointer)})"
441
+ )
384
442
  if has_store:
385
- components.append("store")
443
+ components.append(f"store (type {type(graph.store)})")
386
444
  component_list = " and ".join(components)
387
445
 
388
446
  raise ValueError(
@@ -391,7 +449,7 @@ def _graph_from_spec(spec: GraphSpec) -> GraphValue:
391
449
  f"so providing a custom {component_list} here isn't necessary and will be ignored when deployed.\n\n"
392
450
  f"To simplify your setup and use the built-in persistence, please remove the custom {component_list} "
393
451
  f"from your graph definition. If you are looking to customize which postgres database to connect to,"
394
- " please set the `POSTGRES_URI_CUSTOM` environment variable."
452
+ " please set the `POSTGRES_URI` environment variable."
395
453
  " See https://langchain-ai.github.io/langgraph/cloud/reference/env_var/#postgres_uri_custom for more details."
396
454
  )
397
455
 
@@ -0,0 +1,68 @@
1
+ /// <reference types="./global.d.ts" />
2
+
3
+ import { z } from "zod";
4
+ import * as fs from "node:fs/promises";
5
+ import * as path from "node:path";
6
+ import {
7
+ filterValidGraphSpecs,
8
+ GraphSchema,
9
+ resolveGraph,
10
+ runGraphSchemaWorker,
11
+ } from "./src/graph.mts";
12
+ import { build } from "@langchain/langgraph-ui";
13
+
14
+ const __dirname = new URL(".", import.meta.url).pathname;
15
+
16
+ async function main() {
17
+ const specs = filterValidGraphSpecs(
18
+ z.record(z.string()).parse(JSON.parse(process.env.LANGSERVE_GRAPHS))
19
+ );
20
+
21
+ const GRAPH_SCHEMAS: Record<string, Record<string, GraphSchema> | false> = {};
22
+ let failed = false;
23
+
24
+ try {
25
+ await Promise.all(
26
+ specs.map(async ([graphId, rawSpec]) => {
27
+ console.info(`[${graphId}]: Checking for source file existence`);
28
+ const { resolved, ...spec } = await resolveGraph(rawSpec, {
29
+ onlyFilePresence: true,
30
+ });
31
+
32
+ try {
33
+ console.info(`[${graphId}]: Extracting schema`);
34
+ GRAPH_SCHEMAS[graphId] = await runGraphSchemaWorker(spec, {
35
+ timeoutMs: 120_000,
36
+ });
37
+ } catch (error) {
38
+ console.error(`[${graphId}]: Error extracting schema: ${error}`);
39
+ GRAPH_SCHEMAS[graphId] = false;
40
+ }
41
+ })
42
+ );
43
+
44
+ await fs.writeFile(
45
+ path.resolve(__dirname, "client.schemas.json"),
46
+ JSON.stringify(GRAPH_SCHEMAS),
47
+ { encoding: "utf-8" }
48
+ );
49
+ } catch (error) {
50
+ console.error(`Error resolving graphs: ${error}`);
51
+ failed = true;
52
+ }
53
+
54
+ // Build Gen UI assets
55
+ try {
56
+ console.info("Checking for UI assets");
57
+ await fs.mkdir(path.resolve(__dirname, "ui"), { recursive: true });
58
+
59
+ await build({ output: path.resolve(__dirname, "ui") });
60
+ } catch (error) {
61
+ console.error(`Error building UI: ${error}`);
62
+ failed = true;
63
+ }
64
+
65
+ if (failed) process.exit(1);
66
+ }
67
+
68
+ main();
@@ -13,6 +13,7 @@ import {
13
13
  Item,
14
14
  Operation,
15
15
  Command,
16
+ Send,
16
17
  OperationResults,
17
18
  type Checkpoint,
18
19
  type CheckpointMetadata,
@@ -539,7 +540,19 @@ const StreamEventsPayload = z.object({
539
540
  graph_name: z.string().nullish(),
540
541
  graph_config: RunnableConfigSchema.nullish(),
541
542
  input: z.unknown(),
542
- command: z.object({ resume: z.unknown() }).nullish(),
543
+ command: z
544
+ .object({
545
+ resume: z.unknown().nullish(),
546
+ goto: z.custom<Send | string | (Send | string)[]>().nullish(),
547
+ graph: z.string().nullish(),
548
+ update: z
549
+ .union([
550
+ z.record(z.unknown()),
551
+ z.array(z.tuple([z.string(), z.unknown()])),
552
+ ])
553
+ .nullish(),
554
+ })
555
+ .nullish(),
543
556
  stream_mode: z
544
557
  .union([ExtraStreamModeSchema, z.array(ExtraStreamModeSchema)])
545
558
  .optional(),
@@ -549,14 +562,33 @@ const StreamEventsPayload = z.object({
549
562
  subgraphs: z.boolean().optional(),
550
563
  });
551
564
 
565
+ function reviveCommand(
566
+ command: z.infer<typeof StreamEventsPayload>["command"]
567
+ ): Command | undefined {
568
+ if (command == null) return undefined;
569
+ let { goto, update, resume, graph } = command;
570
+
571
+ goto ??= undefined;
572
+ update ??= undefined;
573
+ resume ??= undefined;
574
+ graph ??= undefined;
575
+
576
+ if (goto != null && !Array.isArray(goto)) goto = [goto];
577
+ goto = goto?.map((item) => {
578
+ if (typeof item === "string") return item;
579
+ return new Send(item.node, item.args);
580
+ });
581
+
582
+ return new Command({ goto, update, resume, graph });
583
+ }
584
+
552
585
  async function* streamEventsRequest(
553
586
  rawPayload: z.infer<typeof StreamEventsPayload>
554
587
  ) {
555
588
  const { graph_id: graphId, ...payload } = rawPayload;
556
589
  const config = getRunnableConfig(payload.config);
557
590
  const graph = await getGraph(graphId, config, payload.graph_name);
558
-
559
- const input = payload.command ? new Command(payload.command) : payload.input;
591
+ const input = reviveCommand(payload.command) ?? payload.input;
560
592
 
561
593
  const userStreamMode =
562
594
  payload.stream_mode == null
@@ -24,7 +24,8 @@
24
24
  "undici": "^6.21.1",
25
25
  "uuid": "^10.0.0",
26
26
  "winston": "^3.17.0",
27
- "@langchain/langgraph-api": "~0.0.19",
27
+ "@langchain/langgraph-api": "~0.0.20",
28
+ "@langchain/langgraph-ui": "~0.0.20",
28
29
  "zod": "^3.23.8"
29
30
  },
30
31
  "resolutions": {
@@ -23,7 +23,7 @@ from langchain_core.runnables.schema import (
23
23
  from langgraph.checkpoint.serde.base import SerializerProtocol
24
24
  from langgraph.pregel.types import PregelTask, StateSnapshot
25
25
  from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
26
- from langgraph.types import Command, Interrupt
26
+ from langgraph.types import Command, Interrupt, Send
27
27
  from pydantic import BaseModel
28
28
  from starlette.applications import Starlette
29
29
  from starlette.exceptions import HTTPException
@@ -58,6 +58,12 @@ _client = httpx.AsyncClient(
58
58
  )
59
59
 
60
60
 
61
+ def default_command(obj):
62
+ if isinstance(obj, Send):
63
+ return {"node": obj.node, "args": obj.arg}
64
+ raise TypeError
65
+
66
+
61
67
  async def _client_stream(method: str, data: dict[str, Any]):
62
68
  graph_id = data.get("graph_id")
63
69
  async with _client.stream(
@@ -68,7 +74,7 @@ async def _client_stream(method: str, data: dict[str, Any]):
68
74
  "Cache-Control": "no-store",
69
75
  "Content-Type": "application/json",
70
76
  },
71
- data=orjson.dumps(data),
77
+ data=orjson.dumps(data, default=default_command),
72
78
  ) as response:
73
79
  decoder = SSEDecoder()
74
80
  async for line in aiter_lines_raw(response):
@@ -84,7 +90,7 @@ async def _client_invoke(method: str, data: dict[str, Any]):
84
90
  res = await _client.post(
85
91
  f"/{graph_id}/{method}",
86
92
  headers={"Content-Type": "application/json"},
87
- data=orjson.dumps(data),
93
+ data=orjson.dumps(data, default=default_command),
88
94
  )
89
95
  return res.json()
90
96