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.
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/PKG-INFO +3 -3
- langgraph_api-0.0.47/langgraph_api/__init__.py +1 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/ui.py +19 -14
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/cli.py +9 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/config.py +3 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/graph.py +80 -22
- langgraph_api-0.0.47/langgraph_api/js/build.mts +68 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/client.mts +35 -3
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/package.json +2 -1
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/remote.py +9 -3
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/api.test.mts +169 -76
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/compose-postgres.yml +2 -1
- langgraph_api-0.0.47/langgraph_api/js/tests/graphs/command.mts +48 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/langgraph.json +2 -1
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/package.json +3 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/yarn.lock +5 -0
- langgraph_api-0.0.47/langgraph_api/js/ui.py +93 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/yarn.lock +26 -6
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/lifespan.py +3 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/patch.py +4 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/openapi.json +8 -1
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/pyproject.toml +3 -3
- langgraph_api-0.0.45/langgraph_api/__init__.py +0 -1
- langgraph_api-0.0.45/langgraph_api/js/build.mts +0 -105
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/LICENSE +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/README.md +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/__init__.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/assistants.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/mcp.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/meta.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/openapi.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/runs.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/store.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/api/threads.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/asyncio.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/auth/__init__.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/auth/custom.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/auth/langsmith/__init__.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/auth/langsmith/backend.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/auth/langsmith/client.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/auth/middleware.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/auth/noop.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/auth/studio_user.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/command.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/cron_scheduler.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/errors.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/http.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/.gitignore +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/base.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/errors.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/global.d.ts +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/schema.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/graph.mts +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/hooks.mjs +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/parser/parser.mts +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/parser/parser.worker.mjs +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/schema/types.mts +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/schema/types.template.mts +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/utils/importMap.mts +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/src/utils/serde.mts +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/sse.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/.gitignore +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/agent.css +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/agent.mts +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/agent.ui.tsx +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/delay.mts +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/dynamic.mts +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/error.mts +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/nested.mts +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/graphs/weather.mts +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/parser.test.mts +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/js/tests/utils.mts +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/logging.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/metadata.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/middleware/__init__.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/middleware/http_logger.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/middleware/private_network.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/models/__init__.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/models/run.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/queue_entrypoint.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/route.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/schema.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/serde.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/server.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/sse.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/state.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/stream.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/thread_ttl.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/utils.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/validation.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/webhook.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_api/worker.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_license/__init__.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_license/middleware.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_license/validation.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_storage/__init__.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_storage/checkpoint.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_storage/database.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_storage/inmem_stream.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_storage/ops.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_storage/queue.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_storage/retry.py +0 -0
- {langgraph_api-0.0.45 → langgraph_api-0.0.47}/langgraph_storage/store.py +0 -0
- {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.
|
|
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 (>=
|
|
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.
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
|
|
@@ -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(
|
|
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
|
-
|
|
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 =
|
|
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=
|
|
210
|
-
|
|
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=
|
|
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(
|
|
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(
|
|
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("
|
|
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
|
|
379
|
-
has_store = graph.store
|
|
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(
|
|
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 `
|
|
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
|
|
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
|
|
@@ -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
|
|