langgraph-api 0.15.0.dev5__py3-none-any.whl → 0.16.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- langgraph_api/__init__.py +1 -1
- langgraph_api/api/a2a.py +56 -12
- langgraph_api/api/a2ui.py +18 -8
- langgraph_api/api/mcp/_handlers.py +3 -1
- langgraph_api/auth/langsmith/backend.py +2 -0
- langgraph_api/auth/studio_user.py +6 -1
- langgraph_api/config/__init__.py +36 -0
- langgraph_api/cron_scheduler.py +1 -1
- langgraph_api/encryption/middleware.py +4 -0
- langgraph_api/graph.py +14 -2
- langgraph_api/js/client.mts +18 -6
- langgraph_api/js/package.json +3 -3
- langgraph_api/js/yarn.lock +103 -158
- langgraph_api/metrics_collector.py +6 -0
- langgraph_api/release_tags.py +35 -17
- langgraph_api/runner/__init__.py +10 -0
- langgraph_api/runner/__main__.py +275 -0
- langgraph_api/self_hosted_logs.py +4 -0
- langgraph_api/worker.py +36 -163
- langgraph_api/worker_runtime.py +221 -0
- {langgraph_api-0.15.0.dev5.dist-info → langgraph_api-0.16.0.dev1.dist-info}/METADATA +2 -2
- {langgraph_api-0.15.0.dev5.dist-info → langgraph_api-0.16.0.dev1.dist-info}/RECORD +30 -23
- langgraph_grpc_common/proto/__init__.py +4 -0
- langgraph_grpc_common/proto/runner_pb2.py +50 -0
- langgraph_grpc_common/proto/runner_pb2.pyi +167 -0
- langgraph_grpc_common/proto/runner_pb2_grpc.py +108 -0
- langgraph_grpc_common/proto/runner_pb2_grpc.pyi +73 -0
- {langgraph_api-0.15.0.dev5.dist-info → langgraph_api-0.16.0.dev1.dist-info}/WHEEL +0 -0
- {langgraph_api-0.15.0.dev5.dist-info → langgraph_api-0.16.0.dev1.dist-info}/entry_points.txt +0 -0
- {langgraph_api-0.15.0.dev5.dist-info → langgraph_api-0.16.0.dev1.dist-info}/licenses/LICENSE +0 -0
langgraph_api/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.16.0.dev1"
|
langgraph_api/api/a2a.py
CHANGED
|
@@ -55,6 +55,8 @@ MAX_HISTORY_LENGTH_REQUESTED = 10
|
|
|
55
55
|
LANGGRAPH_HISTORY_QUERY_LIMIT = 500
|
|
56
56
|
HISTORY_SCOPE_EXTENSION_URI = "https://langchain.com/a2a/extensions/history-scope/v1"
|
|
57
57
|
HISTORY_SCOPE_HEADER = "LangGraph-A2A-History-Scope"
|
|
58
|
+
A2A_EXTENSIONS_HEADER = "A2A-Extensions"
|
|
59
|
+
A2UI_EXTENSIONS_HEADER = "X-A2A-Extensions"
|
|
58
60
|
HistoryScope = Literal["context", "task"]
|
|
59
61
|
|
|
60
62
|
|
|
@@ -123,6 +125,7 @@ class A2UIContext:
|
|
|
123
125
|
metadata: dict[str, Any]
|
|
124
126
|
extensions: list[str]
|
|
125
127
|
drop_invalid: bool = False
|
|
128
|
+
mime_type: str = A2UI_MIME_TYPE
|
|
126
129
|
|
|
127
130
|
|
|
128
131
|
_A2UI_DISABLED_CONTEXT = A2UIContext(False, False, {}, [])
|
|
@@ -170,6 +173,11 @@ def _a2ui_context_for_run(
|
|
|
170
173
|
active=a2a_metadata.get("a2ui_active") is True,
|
|
171
174
|
metadata={},
|
|
172
175
|
extensions=[],
|
|
176
|
+
mime_type=(
|
|
177
|
+
a2a_metadata["a2ui_mime_type"]
|
|
178
|
+
if is_a2ui_mime_type(a2a_metadata.get("a2ui_mime_type"))
|
|
179
|
+
else A2UI_MIME_TYPE
|
|
180
|
+
),
|
|
173
181
|
)
|
|
174
182
|
|
|
175
183
|
|
|
@@ -197,26 +205,46 @@ def _a2a_run_metadata(
|
|
|
197
205
|
**({"task_id": task_id} if task_id else {}),
|
|
198
206
|
**({"task_start_id": task_start_id} if task_start_id else {}),
|
|
199
207
|
**({"a2ui_active": context.active} if context.enabled else {}),
|
|
208
|
+
**(
|
|
209
|
+
{"a2ui_mime_type": context.mime_type}
|
|
210
|
+
if context.enabled and context.active
|
|
211
|
+
else {}
|
|
212
|
+
),
|
|
200
213
|
}
|
|
201
214
|
}
|
|
202
215
|
|
|
203
216
|
|
|
204
217
|
def _a2ui_message_context(
|
|
205
|
-
|
|
218
|
+
params: dict[str, Any],
|
|
219
|
+
assistant: dict[str, Any],
|
|
220
|
+
headers: Headers,
|
|
221
|
+
*,
|
|
222
|
+
default_mime_type: str = A2UI_MIME_TYPE,
|
|
206
223
|
) -> A2UIContext:
|
|
224
|
+
message = params.get("message") or {}
|
|
207
225
|
metadata = message.get("metadata") or {}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
226
|
+
if not isinstance(metadata, dict):
|
|
227
|
+
raise A2UIValidationError("Invalid A2A message metadata")
|
|
228
|
+
extensions: list[str] = []
|
|
229
|
+
for header in (A2A_EXTENSIONS_HEADER, A2UI_EXTENSIONS_HEADER):
|
|
230
|
+
for header_value in headers.getlist(header):
|
|
231
|
+
for value in header_value.split(","):
|
|
232
|
+
if (extension := value.strip()) and extension not in extensions:
|
|
233
|
+
extensions.append(extension)
|
|
234
|
+
mime_type = default_mime_type
|
|
235
|
+
for part in message.get("parts") or []:
|
|
236
|
+
part_metadata = part.get("metadata") if isinstance(part, dict) else None
|
|
237
|
+
if isinstance(part_metadata, dict) and is_a2ui_mime_type(
|
|
238
|
+
part_metadata.get("mimeType")
|
|
239
|
+
):
|
|
240
|
+
mime_type = part_metadata["mimeType"]
|
|
241
|
+
break
|
|
215
242
|
return A2UIContext(
|
|
216
243
|
enabled=_a2ui_enabled(assistant),
|
|
217
244
|
active=A2UI_EXTENSION_URI in extensions,
|
|
218
245
|
metadata=metadata,
|
|
219
246
|
extensions=extensions,
|
|
247
|
+
mime_type=mime_type,
|
|
220
248
|
)
|
|
221
249
|
|
|
222
250
|
|
|
@@ -892,7 +920,9 @@ def _process_a2a_message_parts(
|
|
|
892
920
|
{
|
|
893
921
|
"type": "data",
|
|
894
922
|
"data": part_data,
|
|
895
|
-
"metadata": normalize_metadata(
|
|
923
|
+
"metadata": normalize_metadata(
|
|
924
|
+
part_metadata, mime_type=a2ui.mime_type
|
|
925
|
+
),
|
|
896
926
|
}
|
|
897
927
|
)
|
|
898
928
|
continue
|
|
@@ -1114,7 +1144,7 @@ def _content_to_a2a_parts(
|
|
|
1114
1144
|
{
|
|
1115
1145
|
"kind": "data",
|
|
1116
1146
|
"data": data,
|
|
1117
|
-
"metadata": normalize_metadata(metadata),
|
|
1147
|
+
"metadata": normalize_metadata(metadata, mime_type=a2ui.mime_type),
|
|
1118
1148
|
}
|
|
1119
1149
|
)
|
|
1120
1150
|
elif (
|
|
@@ -2095,6 +2125,7 @@ async def handle_message_send(
|
|
|
2095
2125
|
task_id = None
|
|
2096
2126
|
task_start_id = str(uuid7())
|
|
2097
2127
|
task_ids_by_start_id: dict[str, str] = {}
|
|
2128
|
+
a2ui_mime_type = A2UI_MIME_TYPE
|
|
2098
2129
|
else:
|
|
2099
2130
|
task = await _resolve_task(
|
|
2100
2131
|
client,
|
|
@@ -2121,10 +2152,16 @@ async def handle_message_send(
|
|
|
2121
2152
|
task_id = task.task_id
|
|
2122
2153
|
task_start_id = None
|
|
2123
2154
|
task_ids_by_start_id = task.task_ids_by_start_id
|
|
2155
|
+
a2ui_mime_type = _a2ui_context_for_run(task.run).mime_type
|
|
2124
2156
|
|
|
2125
2157
|
# Process A2A message parts into LangChain messages format
|
|
2126
2158
|
try:
|
|
2127
|
-
a2ui = _a2ui_message_context(
|
|
2159
|
+
a2ui = _a2ui_message_context(
|
|
2160
|
+
params,
|
|
2161
|
+
assistant,
|
|
2162
|
+
request.headers,
|
|
2163
|
+
default_mime_type=a2ui_mime_type,
|
|
2164
|
+
)
|
|
2128
2165
|
message_role = _normalize_input_role(message.get("role", "ROLE_USER"))
|
|
2129
2166
|
input_content = _process_a2a_message_parts(
|
|
2130
2167
|
parts,
|
|
@@ -3352,6 +3389,7 @@ async def handle_message_stream(
|
|
|
3352
3389
|
context_id = context_id_from_message or str(uuid.uuid4())
|
|
3353
3390
|
task_id = None
|
|
3354
3391
|
task_start_id = str(uuid7())
|
|
3392
|
+
a2ui_mime_type = A2UI_MIME_TYPE
|
|
3355
3393
|
else:
|
|
3356
3394
|
task = await _resolve_task(
|
|
3357
3395
|
client,
|
|
@@ -3389,10 +3427,16 @@ async def handle_message_stream(
|
|
|
3389
3427
|
context_id = task.context_id
|
|
3390
3428
|
task_id = task.task_id
|
|
3391
3429
|
task_start_id = None
|
|
3430
|
+
a2ui_mime_type = _a2ui_context_for_run(task.run).mime_type
|
|
3392
3431
|
|
|
3393
3432
|
# Process A2A message parts into LangChain messages format
|
|
3394
3433
|
try:
|
|
3395
|
-
a2ui = _a2ui_message_context(
|
|
3434
|
+
a2ui = _a2ui_message_context(
|
|
3435
|
+
params,
|
|
3436
|
+
assistant,
|
|
3437
|
+
request.headers,
|
|
3438
|
+
default_mime_type=a2ui_mime_type,
|
|
3439
|
+
)
|
|
3396
3440
|
message_role = _normalize_input_role(message.get("role", "ROLE_USER"))
|
|
3397
3441
|
input_content = _process_a2a_message_parts(
|
|
3398
3442
|
parts,
|
langgraph_api/api/a2ui.py
CHANGED
|
@@ -9,8 +9,8 @@ import jsonschema_rs
|
|
|
9
9
|
import orjson
|
|
10
10
|
|
|
11
11
|
A2UI_EXTENSION_URI = "https://a2ui.org/a2a-extension/a2ui/v0.9"
|
|
12
|
-
A2UI_MIME_TYPE = "application/a2ui
|
|
13
|
-
_COMPAT_A2UI_MIME_TYPE = "application/json
|
|
12
|
+
A2UI_MIME_TYPE = "application/json+a2ui"
|
|
13
|
+
_COMPAT_A2UI_MIME_TYPE = "application/a2ui+json"
|
|
14
14
|
_SCHEMA_DIR = Path(__file__).with_name("a2ui_schemas")
|
|
15
15
|
|
|
16
16
|
|
|
@@ -52,9 +52,11 @@ def is_a2ui_mime_type(value: Any) -> bool:
|
|
|
52
52
|
return value in {A2UI_MIME_TYPE, _COMPAT_A2UI_MIME_TYPE}
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
def normalize_metadata(
|
|
55
|
+
def normalize_metadata(
|
|
56
|
+
metadata: dict[str, Any], *, mime_type: str = A2UI_MIME_TYPE
|
|
57
|
+
) -> dict[str, Any]:
|
|
56
58
|
if is_a2ui_mime_type(metadata.get("mimeType")):
|
|
57
|
-
return {**metadata, "mimeType":
|
|
59
|
+
return {**metadata, "mimeType": mime_type}
|
|
58
60
|
return metadata
|
|
59
61
|
|
|
60
62
|
|
|
@@ -70,9 +72,17 @@ def validate_client_metadata(metadata: dict[str, Any]) -> None:
|
|
|
70
72
|
raise A2UIValidationError("Invalid A2UI message metadata") from exc
|
|
71
73
|
|
|
72
74
|
|
|
73
|
-
def validate_payload(
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
def validate_payload(
|
|
76
|
+
data: Any,
|
|
77
|
+
*,
|
|
78
|
+
direction: Literal["client", "server"],
|
|
79
|
+
) -> None:
|
|
80
|
+
if isinstance(data, list):
|
|
81
|
+
messages = data
|
|
82
|
+
elif isinstance(data, dict):
|
|
83
|
+
messages = (data,)
|
|
84
|
+
else:
|
|
85
|
+
raise A2UIValidationError("A2UI DataPart data must be an array or object")
|
|
76
86
|
|
|
77
87
|
validator = (
|
|
78
88
|
_CLIENT_MESSAGE_VALIDATOR
|
|
@@ -80,7 +90,7 @@ def validate_payload(data: Any, *, direction: Literal["client", "server"]) -> No
|
|
|
80
90
|
else _SERVER_MESSAGE_VALIDATOR
|
|
81
91
|
)
|
|
82
92
|
try:
|
|
83
|
-
for message in
|
|
93
|
+
for message in messages:
|
|
84
94
|
validator.validate(message)
|
|
85
95
|
except ValueError as exc:
|
|
86
96
|
raise A2UIValidationError(f"Invalid A2UI {direction} message") from exc
|
|
@@ -287,7 +287,9 @@ async def handle_tools_list(
|
|
|
287
287
|
schemas = await client.assistants.get_schemas(
|
|
288
288
|
assistant["assistant_id"], headers=request.headers
|
|
289
289
|
)
|
|
290
|
-
input_schema = schemas.get("input_schema"
|
|
290
|
+
input_schema = schemas.get("input_schema", {})
|
|
291
|
+
if input_schema is None:
|
|
292
|
+
raise ValueError("Input schema generation failed")
|
|
291
293
|
if input_schema:
|
|
292
294
|
_sanitizers.simplify_mcp_schema_inplace(input_schema)
|
|
293
295
|
# MCP spec requires inputSchema to be a JSON Schema with type "object"
|
|
@@ -21,6 +21,7 @@ class AuthDict(TypedDict):
|
|
|
21
21
|
organization_id: str
|
|
22
22
|
tenant_id: str
|
|
23
23
|
user_id: NotRequired[str]
|
|
24
|
+
ls_user_id: NotRequired[str]
|
|
24
25
|
user_email: NotRequired[str]
|
|
25
26
|
|
|
26
27
|
|
|
@@ -94,6 +95,7 @@ class LangsmithAuthBackend(AuthenticationBackend):
|
|
|
94
95
|
|
|
95
96
|
credentials = AuthCredentials(["authenticated"])
|
|
96
97
|
user = StudioUser(auth_dict.get("user_id"), is_authenticated=True)
|
|
98
|
+
user.ls_user_id = auth_dict.get("ls_user_id")
|
|
97
99
|
|
|
98
100
|
# Cache the result
|
|
99
101
|
self._cache.set(cache_key, AuthCacheEntry(credentials=credentials, user=user))
|
|
@@ -5,14 +5,19 @@ from starlette.authentication import BaseUser
|
|
|
5
5
|
class StudioUser(StudioUserBase, BaseUser):
|
|
6
6
|
"""StudioUser class."""
|
|
7
7
|
|
|
8
|
+
ls_user_id: str | None = None
|
|
9
|
+
|
|
8
10
|
def dict(self):
|
|
9
|
-
|
|
11
|
+
user = {
|
|
10
12
|
"kind": "StudioUser",
|
|
11
13
|
"is_authenticated": self.is_authenticated,
|
|
12
14
|
"display_name": self.display_name,
|
|
13
15
|
"identity": self.identity,
|
|
14
16
|
"permissions": self.permissions,
|
|
15
17
|
}
|
|
18
|
+
if self.ls_user_id is not None:
|
|
19
|
+
user["ls_user_id"] = self.ls_user_id
|
|
20
|
+
return user
|
|
16
21
|
|
|
17
22
|
def __getitem__(self, key):
|
|
18
23
|
return self.dict()[key]
|
langgraph_api/config/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from enum import StrEnum
|
|
2
3
|
from os import environ, getenv
|
|
3
4
|
from typing import TYPE_CHECKING, Annotated, Literal, TypeVar, cast
|
|
4
5
|
|
|
@@ -398,6 +399,35 @@ if THREAD_TTL is None and CHECKPOINTER_CONFIG is not None:
|
|
|
398
399
|
THREAD_TTL = CHECKPOINTER_CONFIG.get("ttl")
|
|
399
400
|
|
|
400
401
|
N_JOBS_PER_WORKER = env("N_JOBS_PER_WORKER", cast=int, default=10)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class DispatchMode(StrEnum):
|
|
405
|
+
"""How background runs are dispatched.
|
|
406
|
+
|
|
407
|
+
single-process: Python queue loop in the API process (default).
|
|
408
|
+
multi-process: Go queue worker claims runs and dispatches to Python runners.
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
SINGLE_PROCESS = "single-process"
|
|
412
|
+
MULTI_PROCESS = "multi-process"
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _parse_dispatch_mode(value: str) -> DispatchMode:
|
|
416
|
+
try:
|
|
417
|
+
return DispatchMode(value)
|
|
418
|
+
except ValueError:
|
|
419
|
+
allowed = ", ".join(repr(mode.value) for mode in DispatchMode)
|
|
420
|
+
raise ValueError(
|
|
421
|
+
f"Invalid DISPATCH_MODE={value!r}; expected one of: {allowed}"
|
|
422
|
+
) from None
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# Not in public docs: infrastructure dispatch mode
|
|
426
|
+
DISPATCH_MODE: DispatchMode = env(
|
|
427
|
+
"DISPATCH_MODE",
|
|
428
|
+
cast=_parse_dispatch_mode,
|
|
429
|
+
default=DispatchMode.SINGLE_PROCESS,
|
|
430
|
+
)
|
|
401
431
|
BG_JOB_TIMEOUT_SECS = env("BG_JOB_TIMEOUT_SECS", cast=float, default=86400)
|
|
402
432
|
STREAM_PUBLISH_RETRY_MAX_DURATION_SECS = env(
|
|
403
433
|
"LSD_STREAM_PUBLISH_RETRY_MAX_DURATION_SECS",
|
|
@@ -620,7 +650,9 @@ EXPOSE_INTERNAL_METRICS_PROMETHEUS = env(
|
|
|
620
650
|
|
|
621
651
|
# These are only set for internal agents (polly/insights/engine/etc..) deployed in byoc so we can fetch logs (see api/langgraph_api/self_hosted_logs.py)
|
|
622
652
|
DATA_PLANE_ID: str | None = env("DATA_PLANE_ID", cast=str, default=None)
|
|
653
|
+
DATA_PLANE_NAME: str | None = env("DATA_PLANE_NAME", cast=str, default=None)
|
|
623
654
|
DATA_PLANE_ORG_ID: str | None = env("DATA_PLANE_ORG_ID", cast=str, default=None)
|
|
655
|
+
DATA_PLANE_ORG_NAME: str | None = env("DATA_PLANE_ORG_NAME", cast=str, default=None)
|
|
624
656
|
DATA_PLANE_JWT_SECRET: str | None = env("DATA_PLANE_JWT_SECRET", cast=str, default=None)
|
|
625
657
|
BYOC_HOST = env("BYOC_HOST", cast=str, default="byoc")
|
|
626
658
|
BYOC_ENV = env("BYOC_ENV", cast=str, default="byoc")
|
|
@@ -687,8 +719,11 @@ __all__ = [
|
|
|
687
719
|
"DATADOG_METRICS_ENABLED",
|
|
688
720
|
"DATA_PLANE_ID",
|
|
689
721
|
"DATA_PLANE_JWT_SECRET",
|
|
722
|
+
"DATA_PLANE_NAME",
|
|
690
723
|
"DATA_PLANE_ORG_ID",
|
|
724
|
+
"DATA_PLANE_ORG_NAME",
|
|
691
725
|
"DB_MIGRATION_BY_CORE_API",
|
|
726
|
+
"DISPATCH_MODE",
|
|
692
727
|
"EXPOSE_INTERNAL_METRICS_PROMETHEUS",
|
|
693
728
|
"FF_CRONS_ENABLED",
|
|
694
729
|
"FF_LOG_DROPPED_EVENTS",
|
|
@@ -769,6 +804,7 @@ __all__ = [
|
|
|
769
804
|
"AuthConfig",
|
|
770
805
|
"CheckpointerConfig",
|
|
771
806
|
"CorsConfig",
|
|
807
|
+
"DispatchMode",
|
|
772
808
|
"HttpConfig",
|
|
773
809
|
"SerdeConfig",
|
|
774
810
|
"StoreConfig",
|
langgraph_api/cron_scheduler.py
CHANGED
|
@@ -15,7 +15,7 @@ from langgraph_api.schema import Cron
|
|
|
15
15
|
from langgraph_api.serde import json_loads
|
|
16
16
|
from langgraph_api.utils import next_cron_date
|
|
17
17
|
from langgraph_api.utils.config import run_in_executor
|
|
18
|
-
from langgraph_api.
|
|
18
|
+
from langgraph_api.worker_runtime import set_auth_ctx_for_run
|
|
19
19
|
from langgraph_runtime.database import connect
|
|
20
20
|
from langgraph_runtime.retry import retry_db
|
|
21
21
|
|
|
@@ -18,6 +18,7 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
|
|
18
18
|
from starlette.requests import Request # noqa: TC002
|
|
19
19
|
|
|
20
20
|
from langgraph_api.auth.noop import UnauthenticatedUser
|
|
21
|
+
from langgraph_api.auth.studio_user import StudioUser
|
|
21
22
|
from langgraph_api.config import LANGGRAPH_ENCRYPTION
|
|
22
23
|
from langgraph_api.encryption.aes_json import (
|
|
23
24
|
AesEncryptionInstance,
|
|
@@ -74,6 +75,9 @@ def _serialize_user_for_encryption(user: BaseUser) -> dict[str, Any]:
|
|
|
74
75
|
if hasattr(user, "model_dump") and callable(user.model_dump):
|
|
75
76
|
return cast("dict[str, Any]", user.model_dump())
|
|
76
77
|
|
|
78
|
+
if isinstance(user, StudioUser):
|
|
79
|
+
return user.dict()
|
|
80
|
+
|
|
77
81
|
# Plain BaseUser subclasses - extract the required properties
|
|
78
82
|
return {
|
|
79
83
|
"identity": user.identity,
|
langgraph_api/graph.py
CHANGED
|
@@ -502,8 +502,14 @@ def _load_graph_config_from_env() -> dict | None:
|
|
|
502
502
|
return config_per_id
|
|
503
503
|
|
|
504
504
|
|
|
505
|
-
async def collect_graphs_from_env(
|
|
506
|
-
|
|
505
|
+
async def collect_graphs_from_env(
|
|
506
|
+
register: bool = False, *, allow_js: bool = True
|
|
507
|
+
) -> None:
|
|
508
|
+
"""Return graphs from env.
|
|
509
|
+
|
|
510
|
+
When ``allow_js`` is False (Python process runner), raise if any JS graph
|
|
511
|
+
specs are configured instead of starting the JS sidecar.
|
|
512
|
+
"""
|
|
507
513
|
|
|
508
514
|
# Not in public docs: LANGSERVE_GRAPHS is internal, set by CLI from langgraph.json "graphs" field
|
|
509
515
|
paths_str = os.getenv("LANGSERVE_GRAPHS")
|
|
@@ -595,6 +601,12 @@ async def collect_graphs_from_env(register: bool = False) -> None:
|
|
|
595
601
|
js_specs = list(filter(is_js_spec, specs))
|
|
596
602
|
py_specs = list(filterfalse(is_js_spec, specs))
|
|
597
603
|
|
|
604
|
+
if js_specs and not allow_js:
|
|
605
|
+
raise NotImplementedError(
|
|
606
|
+
"JS graphs are not supported in the Python process runner. "
|
|
607
|
+
f"Configured JS graph ids: {[s.id for s in js_specs]}"
|
|
608
|
+
)
|
|
609
|
+
|
|
598
610
|
if js_specs:
|
|
599
611
|
if lg_api_config.API_VARIANT == "local_dev":
|
|
600
612
|
raise NotImplementedError(
|
langgraph_api/js/client.mts
CHANGED
|
@@ -346,7 +346,10 @@ const handleInvoke = <T extends z.ZodType<any>>(
|
|
|
346
346
|
const handleStream = <T extends z.ZodType<any>>(
|
|
347
347
|
name: string,
|
|
348
348
|
_schema: T,
|
|
349
|
-
handler: (
|
|
349
|
+
handler: (
|
|
350
|
+
rawPayload: z.infer<T>,
|
|
351
|
+
signal: AbortSignal,
|
|
352
|
+
) => AsyncGenerator<any, void, unknown>,
|
|
350
353
|
) => {
|
|
351
354
|
return (c: Context<any, any, { in: z.infer<T>; out: any }>) => {
|
|
352
355
|
const graphId = c.req.param("graphId");
|
|
@@ -370,18 +373,24 @@ const handleStream = <T extends z.ZodType<any>>(
|
|
|
370
373
|
|
|
371
374
|
try {
|
|
372
375
|
timer = setTimeout(sendHeartbeat, HEARTBEAT_MS);
|
|
373
|
-
for await (const data of handler(
|
|
376
|
+
for await (const data of handler(
|
|
377
|
+
{ graph_id: graphId, ...body },
|
|
378
|
+
c.req.raw.signal,
|
|
379
|
+
)) {
|
|
374
380
|
await sendSSE(name, data);
|
|
375
381
|
}
|
|
376
382
|
} catch (error) {
|
|
377
383
|
// Still print out the error, as the stack
|
|
378
384
|
// trace is not carried over in Python
|
|
379
385
|
logger.error(error);
|
|
380
|
-
|
|
386
|
+
// The socket is already gone once the request aborts, so skip the frame.
|
|
387
|
+
if (!c.req.raw.signal.aborted) {
|
|
388
|
+
await sendSSE("error", serializeError(error));
|
|
389
|
+
}
|
|
390
|
+
} finally {
|
|
391
|
+
clearTimeout(timer);
|
|
392
|
+
await resolve;
|
|
381
393
|
}
|
|
382
|
-
|
|
383
|
-
clearTimeout(timer);
|
|
384
|
-
await resolve;
|
|
385
394
|
});
|
|
386
395
|
};
|
|
387
396
|
};
|
|
@@ -682,6 +691,7 @@ function reviveCommand(
|
|
|
682
691
|
|
|
683
692
|
async function* streamEventsRequest(
|
|
684
693
|
rawPayload: z.infer<typeof StreamEventsPayload>,
|
|
694
|
+
signal: AbortSignal,
|
|
685
695
|
) {
|
|
686
696
|
const { graph_id: graphId, ...payload } = rawPayload;
|
|
687
697
|
const config = {
|
|
@@ -783,6 +793,7 @@ async function* streamEventsRequest(
|
|
|
783
793
|
input,
|
|
784
794
|
{
|
|
785
795
|
...config,
|
|
796
|
+
signal,
|
|
786
797
|
version: "v3",
|
|
787
798
|
interruptBefore,
|
|
788
799
|
interruptAfter,
|
|
@@ -803,6 +814,7 @@ async function* streamEventsRequest(
|
|
|
803
814
|
input,
|
|
804
815
|
{
|
|
805
816
|
...config,
|
|
817
|
+
signal,
|
|
806
818
|
version: "v2",
|
|
807
819
|
streamMode,
|
|
808
820
|
subgraphs: payload.subgraphs,
|
langgraph_api/js/package.json
CHANGED
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
"@types/json-schema": "^7.0.15",
|
|
18
18
|
"dedent": "^1.7.2",
|
|
19
19
|
"exit-hook": "^5.1.0",
|
|
20
|
-
"hono": "^4.
|
|
20
|
+
"hono": "^4.13.7",
|
|
21
21
|
"p-queue": "^9.3.1",
|
|
22
22
|
"p-retry": "^7.1.1",
|
|
23
|
+
"typescript": "^5.9.3",
|
|
23
24
|
"undici": "^7.28.0",
|
|
24
25
|
"uuid": "^14.0.1",
|
|
25
26
|
"winston": "^3.19.0",
|
|
@@ -40,9 +41,8 @@
|
|
|
40
41
|
"jose": "^6.2.3",
|
|
41
42
|
"postgres": "^3.4.9",
|
|
42
43
|
"prettier": "^3.9.5",
|
|
43
|
-
"typescript": "^5.9.3",
|
|
44
44
|
"vite": "^8.1.4",
|
|
45
|
-
"vitest": "^
|
|
45
|
+
"vitest": "^5.0.0"
|
|
46
46
|
},
|
|
47
47
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
48
48
|
}
|