langgraph-api 0.0.15__py3-none-any.whl → 0.0.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of langgraph-api might be problematic. Click here for more details.
- langgraph_api/api/assistants.py +49 -17
- langgraph_api/api/store.py +67 -15
- langgraph_api/asyncio.py +5 -0
- langgraph_api/auth/custom.py +25 -6
- langgraph_api/auth/noop.py +9 -1
- langgraph_api/config.py +6 -1
- langgraph_api/cron_scheduler.py +7 -4
- langgraph_api/js/client.mts +5 -1
- langgraph_api/js/client.new.mts +5 -1
- langgraph_api/js/package.json +2 -2
- langgraph_api/js/remote_new.py +10 -2
- langgraph_api/js/remote_old.py +10 -2
- langgraph_api/js/tests/api.test.mts +124 -7
- langgraph_api/js/tests/graphs/agent.mts +58 -3
- langgraph_api/js/tests/graphs/delay.mts +5 -0
- langgraph_api/js/yarn.lock +13 -13
- langgraph_api/models/run.py +9 -2
- langgraph_api/queue.py +58 -8
- langgraph_api/route.py +8 -3
- langgraph_api/schema.py +1 -1
- langgraph_api/stream.py +9 -2
- langgraph_api/utils.py +14 -6
- {langgraph_api-0.0.15.dist-info → langgraph_api-0.0.17.dist-info}/METADATA +2 -2
- {langgraph_api-0.0.15.dist-info → langgraph_api-0.0.17.dist-info}/RECORD +29 -29
- langgraph_storage/ops.py +9 -2
- openapi.json +5 -5
- {langgraph_api-0.0.15.dist-info → langgraph_api-0.0.17.dist-info}/LICENSE +0 -0
- {langgraph_api-0.0.15.dist-info → langgraph_api-0.0.17.dist-info}/WHEEL +0 -0
- {langgraph_api-0.0.15.dist-info → langgraph_api-0.0.17.dist-info}/entry_points.txt +0 -0
langgraph_api/api/assistants.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
from uuid import uuid4
|
|
3
3
|
|
|
4
|
+
import structlog
|
|
4
5
|
from langchain_core.runnables.utils import create_model
|
|
5
6
|
from langgraph.pregel import Pregel
|
|
6
7
|
from starlette.exceptions import HTTPException
|
|
@@ -23,6 +24,8 @@ from langgraph_storage.database import connect
|
|
|
23
24
|
from langgraph_storage.ops import Assistants
|
|
24
25
|
from langgraph_storage.retry import retry_db
|
|
25
26
|
|
|
27
|
+
logger = structlog.stdlib.get_logger(__name__)
|
|
28
|
+
|
|
26
29
|
|
|
27
30
|
def _state_jsonschema(graph: Pregel) -> dict | None:
|
|
28
31
|
fields: dict = {}
|
|
@@ -39,11 +42,17 @@ def _state_jsonschema(graph: Pregel) -> dict | None:
|
|
|
39
42
|
def _graph_schemas(graph: Pregel) -> dict:
|
|
40
43
|
try:
|
|
41
44
|
input_schema = graph.get_input_jsonschema()
|
|
42
|
-
except Exception:
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logger.warning(
|
|
47
|
+
f"Failed to get input schema for graph {graph.name} with error: `{str(e)}`"
|
|
48
|
+
)
|
|
43
49
|
input_schema = None
|
|
44
50
|
try:
|
|
45
51
|
output_schema = graph.get_output_jsonschema()
|
|
46
|
-
except Exception:
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.warning(
|
|
54
|
+
f"Failed to get output schema for graph {graph.name} with error: `{str(e)}`"
|
|
55
|
+
)
|
|
47
56
|
output_schema = None
|
|
48
57
|
state_schema = _state_jsonschema(graph)
|
|
49
58
|
try:
|
|
@@ -52,7 +61,10 @@ def _graph_schemas(graph: Pregel) -> dict:
|
|
|
52
61
|
if "configurable" in graph.config_schema().__fields__
|
|
53
62
|
else {}
|
|
54
63
|
)
|
|
55
|
-
except Exception:
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.warning(
|
|
66
|
+
f"Failed to get config schema for graph {graph.name} with error: `{str(e)}`"
|
|
67
|
+
)
|
|
56
68
|
config_schema = None
|
|
57
69
|
return {
|
|
58
70
|
"input_schema": input_schema,
|
|
@@ -141,7 +153,13 @@ async def get_assistant_graph(
|
|
|
141
153
|
if isinstance(graph, BaseRemotePregel):
|
|
142
154
|
drawable_graph = await graph.fetch_graph(xray=xray)
|
|
143
155
|
return ApiResponse(drawable_graph.to_json())
|
|
144
|
-
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
return ApiResponse(graph.get_graph(xray=xray).to_json())
|
|
159
|
+
except NotImplementedError:
|
|
160
|
+
raise HTTPException(
|
|
161
|
+
422, detail="The graph does not support visualization"
|
|
162
|
+
) from None
|
|
145
163
|
|
|
146
164
|
|
|
147
165
|
@retry_db
|
|
@@ -167,16 +185,21 @@ async def get_assistant_subgraphs(
|
|
|
167
185
|
)
|
|
168
186
|
)
|
|
169
187
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
188
|
+
try:
|
|
189
|
+
return ApiResponse(
|
|
190
|
+
{
|
|
191
|
+
ns: _graph_schemas(subgraph)
|
|
192
|
+
async for ns, subgraph in graph.aget_subgraphs(
|
|
193
|
+
namespace=namespace,
|
|
194
|
+
recurse=request.query_params.get("recurse", "False")
|
|
195
|
+
in ("true", "True"),
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
except NotImplementedError:
|
|
200
|
+
raise HTTPException(
|
|
201
|
+
422, detail="The graph does not support visualization"
|
|
202
|
+
) from None
|
|
180
203
|
|
|
181
204
|
|
|
182
205
|
@retry_db
|
|
@@ -205,11 +228,17 @@ async def get_assistant_schemas(
|
|
|
205
228
|
|
|
206
229
|
try:
|
|
207
230
|
input_schema = graph.get_input_schema().schema()
|
|
208
|
-
except Exception:
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.warning(
|
|
233
|
+
f"Failed to get input schema for graph {graph.name} with error: `{str(e)}`"
|
|
234
|
+
)
|
|
209
235
|
input_schema = None
|
|
210
236
|
try:
|
|
211
237
|
output_schema = graph.get_output_schema().schema()
|
|
212
|
-
except Exception:
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.warning(
|
|
240
|
+
f"Failed to get output schema for graph {graph.name} with error: `{str(e)}`"
|
|
241
|
+
)
|
|
213
242
|
output_schema = None
|
|
214
243
|
|
|
215
244
|
state_schema = _state_jsonschema(graph)
|
|
@@ -219,8 +248,11 @@ async def get_assistant_schemas(
|
|
|
219
248
|
if "configurable" in graph.config_schema().__fields__
|
|
220
249
|
else {}
|
|
221
250
|
)
|
|
222
|
-
except Exception:
|
|
251
|
+
except Exception as e:
|
|
223
252
|
config_schema = None
|
|
253
|
+
logger.warning(
|
|
254
|
+
f"Failed to get config schema for graph {graph.name} with error: `{str(e)}`"
|
|
255
|
+
)
|
|
224
256
|
return ApiResponse(
|
|
225
257
|
{
|
|
226
258
|
"graph_id": assistant["graph_id"],
|
langgraph_api/api/store.py
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from langgraph_sdk.auth import Auth
|
|
1
4
|
from starlette.responses import Response
|
|
2
5
|
from starlette.routing import BaseRoute
|
|
3
6
|
|
|
7
|
+
from langgraph_api.auth.custom import handle_event as _handle_event
|
|
4
8
|
from langgraph_api.route import ApiRequest, ApiResponse, ApiRoute
|
|
9
|
+
from langgraph_api.utils import get_auth_ctx
|
|
5
10
|
from langgraph_api.validation import (
|
|
6
11
|
StoreDeleteRequest,
|
|
7
12
|
StoreListNamespacesRequest,
|
|
@@ -21,6 +26,24 @@ def _validate_namespace(namespace: tuple[str, ...]) -> Response | None:
|
|
|
21
26
|
)
|
|
22
27
|
|
|
23
28
|
|
|
29
|
+
async def handle_event(
|
|
30
|
+
action: str,
|
|
31
|
+
value: Any,
|
|
32
|
+
) -> None:
|
|
33
|
+
ctx = get_auth_ctx()
|
|
34
|
+
if not ctx:
|
|
35
|
+
return
|
|
36
|
+
await _handle_event(
|
|
37
|
+
Auth.types.AuthContext(
|
|
38
|
+
user=ctx.user,
|
|
39
|
+
permissions=ctx.permissions,
|
|
40
|
+
resource="store",
|
|
41
|
+
action=action,
|
|
42
|
+
),
|
|
43
|
+
value,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
24
47
|
@retry_db
|
|
25
48
|
async def put_item(request: ApiRequest):
|
|
26
49
|
"""Store or update an item."""
|
|
@@ -28,9 +51,13 @@ async def put_item(request: ApiRequest):
|
|
|
28
51
|
namespace = tuple(payload["namespace"]) if payload.get("namespace") else ()
|
|
29
52
|
if err := _validate_namespace(namespace):
|
|
30
53
|
return err
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
54
|
+
handler_payload = {
|
|
55
|
+
"namespace": namespace,
|
|
56
|
+
"key": payload["key"],
|
|
57
|
+
"value": payload["value"],
|
|
58
|
+
}
|
|
59
|
+
await handle_event("put", handler_payload)
|
|
60
|
+
await Store().aput(namespace, handler_payload["key"], handler_payload["value"])
|
|
34
61
|
return Response(status_code=204)
|
|
35
62
|
|
|
36
63
|
|
|
@@ -43,6 +70,11 @@ async def get_item(request: ApiRequest):
|
|
|
43
70
|
key = request.query_params.get("key")
|
|
44
71
|
if not key:
|
|
45
72
|
return ApiResponse({"error": "Key is required"}, status_code=400)
|
|
73
|
+
handler_payload = {
|
|
74
|
+
"namespace": namespace,
|
|
75
|
+
"key": key,
|
|
76
|
+
}
|
|
77
|
+
await handle_event("get", handler_payload)
|
|
46
78
|
result = await Store().aget(namespace, key)
|
|
47
79
|
return ApiResponse(result.dict() if result is not None else None)
|
|
48
80
|
|
|
@@ -54,8 +86,12 @@ async def delete_item(request: ApiRequest):
|
|
|
54
86
|
namespace = tuple(payload["namespace"]) if payload.get("namespace") else ()
|
|
55
87
|
if err := _validate_namespace(namespace):
|
|
56
88
|
return err
|
|
57
|
-
|
|
58
|
-
|
|
89
|
+
handler_payload = {
|
|
90
|
+
"namespace": namespace,
|
|
91
|
+
"key": payload["key"],
|
|
92
|
+
}
|
|
93
|
+
await handle_event("delete", handler_payload)
|
|
94
|
+
await Store().adelete(handler_payload["namespace"], handler_payload["key"])
|
|
59
95
|
return Response(status_code=204)
|
|
60
96
|
|
|
61
97
|
|
|
@@ -70,12 +106,20 @@ async def search_items(request: ApiRequest):
|
|
|
70
106
|
limit = payload.get("limit") or 10
|
|
71
107
|
offset = payload.get("offset") or 0
|
|
72
108
|
query = payload.get("query")
|
|
109
|
+
handler_payload = {
|
|
110
|
+
"namespace": namespace_prefix,
|
|
111
|
+
"filter": filter,
|
|
112
|
+
"limit": limit,
|
|
113
|
+
"offset": offset,
|
|
114
|
+
"query": query,
|
|
115
|
+
}
|
|
116
|
+
await handle_event("search", handler_payload)
|
|
73
117
|
items = await Store().asearch(
|
|
74
|
-
|
|
75
|
-
filter=filter,
|
|
76
|
-
limit=limit,
|
|
77
|
-
offset=offset,
|
|
78
|
-
query=query,
|
|
118
|
+
handler_payload["namespace"],
|
|
119
|
+
filter=handler_payload["filter"],
|
|
120
|
+
limit=handler_payload["limit"],
|
|
121
|
+
offset=handler_payload["offset"],
|
|
122
|
+
query=handler_payload["query"],
|
|
79
123
|
)
|
|
80
124
|
return ApiResponse({"items": [item.dict() for item in items]})
|
|
81
125
|
|
|
@@ -93,12 +137,20 @@ async def list_namespaces(request: ApiRequest):
|
|
|
93
137
|
max_depth = payload.get("max_depth")
|
|
94
138
|
limit = payload.get("limit", 100)
|
|
95
139
|
offset = payload.get("offset", 0)
|
|
140
|
+
handler_payload = {
|
|
141
|
+
"namespace": prefix,
|
|
142
|
+
"suffix": suffix,
|
|
143
|
+
"max_depth": max_depth,
|
|
144
|
+
"limit": limit,
|
|
145
|
+
"offset": offset,
|
|
146
|
+
}
|
|
147
|
+
await handle_event("list_namespaces", handler_payload)
|
|
96
148
|
result = await Store().alist_namespaces(
|
|
97
|
-
prefix=
|
|
98
|
-
suffix=suffix,
|
|
99
|
-
max_depth=max_depth,
|
|
100
|
-
limit=limit,
|
|
101
|
-
offset=offset,
|
|
149
|
+
prefix=handler_payload["namespace"],
|
|
150
|
+
suffix=handler_payload["suffix"],
|
|
151
|
+
max_depth=handler_payload["max_depth"],
|
|
152
|
+
limit=handler_payload["limit"],
|
|
153
|
+
offset=handler_payload["offset"],
|
|
102
154
|
)
|
|
103
155
|
return ApiResponse({"namespaces": result})
|
|
104
156
|
|
langgraph_api/asyncio.py
CHANGED
|
@@ -60,6 +60,11 @@ async def wait_if_not_done(coro: Coroutine[Any, Any, T], done: ValueEvent) -> T:
|
|
|
60
60
|
try:
|
|
61
61
|
return await coro_task
|
|
62
62
|
except asyncio.CancelledError as e:
|
|
63
|
+
if e.args and asyncio.isfuture(e.args[-1]):
|
|
64
|
+
await logger.ainfo(
|
|
65
|
+
"Awaiting future upon cancellation", task=str(e.args[-1])
|
|
66
|
+
)
|
|
67
|
+
await e.args[-1]
|
|
63
68
|
if e.args and isinstance(e.args[0], Exception):
|
|
64
69
|
raise e.args[0] from None
|
|
65
70
|
raise
|
langgraph_api/auth/custom.py
CHANGED
|
@@ -16,7 +16,6 @@ from starlette.authentication import (
|
|
|
16
16
|
AuthenticationBackend,
|
|
17
17
|
AuthenticationError,
|
|
18
18
|
BaseUser,
|
|
19
|
-
SimpleUser,
|
|
20
19
|
)
|
|
21
20
|
from starlette.concurrency import run_in_threadpool
|
|
22
21
|
from starlette.exceptions import HTTPException
|
|
@@ -398,6 +397,16 @@ class DotDict:
|
|
|
398
397
|
raise AttributeError(f"'DotDict' object has no attribute '{name}'")
|
|
399
398
|
return self._dict[name]
|
|
400
399
|
|
|
400
|
+
def __getitem__(self, key):
|
|
401
|
+
return self._dict[key]
|
|
402
|
+
|
|
403
|
+
def __setitem__(self, key, value):
|
|
404
|
+
self._dict[key] = value
|
|
405
|
+
if isinstance(value, dict):
|
|
406
|
+
setattr(self, key, DotDict(value))
|
|
407
|
+
else:
|
|
408
|
+
setattr(self, key, value)
|
|
409
|
+
|
|
401
410
|
def __deepcopy__(self, memo):
|
|
402
411
|
return DotDict(copy.deepcopy(self._dict))
|
|
403
412
|
|
|
@@ -457,11 +466,22 @@ class ProxyUser(BaseUser):
|
|
|
457
466
|
**d,
|
|
458
467
|
}
|
|
459
468
|
|
|
469
|
+
def __getitem__(self, key):
|
|
470
|
+
return self._user[key]
|
|
471
|
+
|
|
472
|
+
def __setitem__(self, key, value):
|
|
473
|
+
self._user[key] = value
|
|
474
|
+
|
|
460
475
|
def __getattr__(self, name: str) -> Any:
|
|
461
476
|
"""Proxy any other attributes to the underlying user object."""
|
|
462
477
|
return getattr(self._user, name)
|
|
463
478
|
|
|
464
479
|
|
|
480
|
+
class SimpleUser(ProxyUser):
|
|
481
|
+
def __init__(self, username: str):
|
|
482
|
+
super().__init__(DotDict({"identity": username}))
|
|
483
|
+
|
|
484
|
+
|
|
465
485
|
def _normalize_auth_response(
|
|
466
486
|
response: Any,
|
|
467
487
|
) -> tuple[AuthCredentials, BaseUser]:
|
|
@@ -481,10 +501,10 @@ def _normalize_auth_response(
|
|
|
481
501
|
user = response
|
|
482
502
|
permissions = []
|
|
483
503
|
|
|
484
|
-
return AuthCredentials(permissions),
|
|
504
|
+
return AuthCredentials(permissions), normalize_user(user)
|
|
485
505
|
|
|
486
506
|
|
|
487
|
-
def
|
|
507
|
+
def normalize_user(user: Any) -> BaseUser:
|
|
488
508
|
"""Normalize user into a BaseUser instance."""
|
|
489
509
|
if isinstance(user, BaseUser):
|
|
490
510
|
return user
|
|
@@ -571,9 +591,8 @@ def _get_handler(auth: Auth, ctx: Auth.types.AuthContext) -> Auth.types.Handler
|
|
|
571
591
|
]
|
|
572
592
|
for key in keys:
|
|
573
593
|
if key in auth._handlers:
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
] # Get the last defined, most specific handler
|
|
594
|
+
# Get the last defined, most specific handler
|
|
595
|
+
result = auth._handlers[key][-1]
|
|
577
596
|
auth._handler_cache[key] = result
|
|
578
597
|
return result
|
|
579
598
|
if auth._global_handlers:
|
langgraph_api/auth/noop.py
CHANGED
|
@@ -2,11 +2,19 @@ from starlette.authentication import (
|
|
|
2
2
|
AuthCredentials,
|
|
3
3
|
AuthenticationBackend,
|
|
4
4
|
BaseUser,
|
|
5
|
-
|
|
5
|
+
)
|
|
6
|
+
from starlette.authentication import (
|
|
7
|
+
UnauthenticatedUser as StarletteUnauthenticatedUser,
|
|
6
8
|
)
|
|
7
9
|
from starlette.requests import HTTPConnection
|
|
8
10
|
|
|
9
11
|
|
|
12
|
+
class UnauthenticatedUser(StarletteUnauthenticatedUser):
|
|
13
|
+
@property
|
|
14
|
+
def identity(self) -> str:
|
|
15
|
+
return ""
|
|
16
|
+
|
|
17
|
+
|
|
10
18
|
class NoopAuthBackend(AuthenticationBackend):
|
|
11
19
|
async def authenticate(
|
|
12
20
|
self, conn: HTTPConnection
|
langgraph_api/config.py
CHANGED
|
@@ -24,7 +24,12 @@ CORS_ALLOW_ORIGINS = env("CORS_ALLOW_ORIGINS", cast=CommaSeparatedStrings, defau
|
|
|
24
24
|
|
|
25
25
|
# queue
|
|
26
26
|
|
|
27
|
-
BG_JOB_NO_DELAY = env("BG_JOB_NO_DELAY", cast=bool, default=
|
|
27
|
+
BG_JOB_NO_DELAY = env("BG_JOB_NO_DELAY", cast=bool, default=None)
|
|
28
|
+
BG_JOB_DELAY = env("BG_JOB_DELAY", cast=float, default=0.5)
|
|
29
|
+
if BG_JOB_NO_DELAY is True:
|
|
30
|
+
BG_JOB_DELAY = 0
|
|
31
|
+
|
|
32
|
+
|
|
28
33
|
N_JOBS_PER_WORKER = env("N_JOBS_PER_WORKER", cast=int, default=10)
|
|
29
34
|
BG_JOB_TIMEOUT_SECS = env("BG_JOB_TIMEOUT_SECS", cast=float, default=3600)
|
|
30
35
|
FF_CRONS_ENABLED = env("FF_CRONS_ENABLED", cast=bool, default=True)
|
langgraph_api/cron_scheduler.py
CHANGED
|
@@ -3,10 +3,10 @@ from random import random
|
|
|
3
3
|
|
|
4
4
|
import structlog
|
|
5
5
|
from langchain_core.runnables.config import run_in_executor
|
|
6
|
-
from starlette.authentication import SimpleUser
|
|
7
6
|
|
|
8
7
|
from langgraph_api.models.run import create_valid_run
|
|
9
|
-
from langgraph_api.
|
|
8
|
+
from langgraph_api.queue import set_auth_ctx_for_run
|
|
9
|
+
from langgraph_api.utils import next_cron_date
|
|
10
10
|
from langgraph_storage.database import connect
|
|
11
11
|
from langgraph_storage.ops import Crons
|
|
12
12
|
from langgraph_storage.retry import retry_db
|
|
@@ -23,10 +23,13 @@ async def cron_scheduler():
|
|
|
23
23
|
try:
|
|
24
24
|
async with connect() as conn:
|
|
25
25
|
async for cron in Crons.next(conn):
|
|
26
|
-
|
|
26
|
+
run_payload = cron["payload"]
|
|
27
|
+
|
|
28
|
+
async with set_auth_ctx_for_run(
|
|
29
|
+
run_payload, user_id=cron["user_id"]
|
|
30
|
+
):
|
|
27
31
|
logger.debug(f"Scheduling cron run {cron}")
|
|
28
32
|
try:
|
|
29
|
-
run_payload = cron["payload"]
|
|
30
33
|
run = await create_valid_run(
|
|
31
34
|
conn,
|
|
32
35
|
thread_id=(
|
langgraph_api/js/client.mts
CHANGED
|
@@ -446,6 +446,7 @@ const StreamModeSchema = z.union([
|
|
|
446
446
|
const ExtraStreamModeSchema = z.union([
|
|
447
447
|
StreamModeSchema,
|
|
448
448
|
z.literal("messages"),
|
|
449
|
+
z.literal("messages-tuple"),
|
|
449
450
|
]);
|
|
450
451
|
|
|
451
452
|
const StreamEventsPayload = z.object({
|
|
@@ -476,11 +477,14 @@ async function* streamEventsRequest(
|
|
|
476
477
|
? payload.stream_mode
|
|
477
478
|
: [payload.stream_mode];
|
|
478
479
|
|
|
479
|
-
const graphStreamMode: Set<"updates" | "debug" | "values"> =
|
|
480
|
+
const graphStreamMode: Set<"updates" | "debug" | "values" | "messages"> =
|
|
481
|
+
new Set();
|
|
480
482
|
if (payload.stream_mode) {
|
|
481
483
|
for (const mode of userStreamMode) {
|
|
482
484
|
if (mode === "messages") {
|
|
483
485
|
graphStreamMode.add("values");
|
|
486
|
+
} else if (mode === "messages-tuple") {
|
|
487
|
+
graphStreamMode.add("messages");
|
|
484
488
|
} else {
|
|
485
489
|
graphStreamMode.add(mode);
|
|
486
490
|
}
|
langgraph_api/js/client.new.mts
CHANGED
|
@@ -504,6 +504,7 @@ const StreamModeSchema = z.union([
|
|
|
504
504
|
const ExtraStreamModeSchema = z.union([
|
|
505
505
|
StreamModeSchema,
|
|
506
506
|
z.literal("messages"),
|
|
507
|
+
z.literal("messages-tuple"),
|
|
507
508
|
]);
|
|
508
509
|
|
|
509
510
|
const StreamEventsPayload = z.object({
|
|
@@ -534,11 +535,14 @@ async function* streamEventsRequest(
|
|
|
534
535
|
? payload.stream_mode
|
|
535
536
|
: [payload.stream_mode];
|
|
536
537
|
|
|
537
|
-
const graphStreamMode: Set<"updates" | "debug" | "values"> =
|
|
538
|
+
const graphStreamMode: Set<"updates" | "debug" | "values" | "messages"> =
|
|
539
|
+
new Set();
|
|
538
540
|
if (payload.stream_mode) {
|
|
539
541
|
for (const mode of userStreamMode) {
|
|
540
542
|
if (mode === "messages") {
|
|
541
543
|
graphStreamMode.add("values");
|
|
544
|
+
} else if (mode === "messages-tuple") {
|
|
545
|
+
graphStreamMode.add("messages");
|
|
542
546
|
} else {
|
|
543
547
|
graphStreamMode.add(mode);
|
|
544
548
|
}
|
langgraph_api/js/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"@hono/node-server": "^1.12.0",
|
|
11
11
|
"@hono/zod-validator": "^0.2.2",
|
|
12
12
|
"@langchain/core": "^0.3.22",
|
|
13
|
-
"@langchain/langgraph": "^0.2.
|
|
13
|
+
"@langchain/langgraph": "^0.2.35",
|
|
14
14
|
"@types/json-schema": "^7.0.15",
|
|
15
15
|
"@typescript/vfs": "^1.6.0",
|
|
16
16
|
"dedent": "^1.5.3",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"undici": "^6.19.7",
|
|
23
23
|
"uuid": "^10.0.0",
|
|
24
24
|
"winston": "^3.15.0",
|
|
25
|
-
"zeromq": "^6.
|
|
25
|
+
"zeromq": "^6.3.0",
|
|
26
26
|
"zod": "^3.23.8"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
langgraph_api/js/remote_new.py
CHANGED
|
@@ -23,6 +23,7 @@ from langgraph.pregel.types import PregelTask, StateSnapshot
|
|
|
23
23
|
from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
|
|
24
24
|
from langgraph.types import Command, Interrupt
|
|
25
25
|
from pydantic import BaseModel
|
|
26
|
+
from starlette.exceptions import HTTPException
|
|
26
27
|
from zmq.utils.monitor import recv_monitor_message
|
|
27
28
|
|
|
28
29
|
from langgraph_api.js.base import BaseRemotePregel
|
|
@@ -681,5 +682,12 @@ async def js_healthcheck():
|
|
|
681
682
|
try:
|
|
682
683
|
await _client_invoke("ok", {})
|
|
683
684
|
return True
|
|
684
|
-
except (RemoteException, TimeoutError, zmq.error.ZMQBaseError):
|
|
685
|
-
|
|
685
|
+
except (RemoteException, TimeoutError, zmq.error.ZMQBaseError) as exc:
|
|
686
|
+
logger.warning(
|
|
687
|
+
"JS healthcheck failed. Either the JS server is not running or the event loop is blocked by a CPU-intensive task.",
|
|
688
|
+
error=exc,
|
|
689
|
+
)
|
|
690
|
+
raise HTTPException(
|
|
691
|
+
status_code=500,
|
|
692
|
+
detail="JS healthcheck failed. Either the JS server is not running or the event loop is blocked by a CPU-intensive task.",
|
|
693
|
+
) from exc
|
langgraph_api/js/remote_old.py
CHANGED
|
@@ -22,6 +22,7 @@ from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
|
|
|
22
22
|
from langgraph.types import Command, Interrupt
|
|
23
23
|
from pydantic import BaseModel
|
|
24
24
|
from starlette.applications import Starlette
|
|
25
|
+
from starlette.exceptions import HTTPException
|
|
25
26
|
from starlette.requests import Request
|
|
26
27
|
from starlette.routing import Route
|
|
27
28
|
|
|
@@ -653,5 +654,12 @@ async def js_healthcheck():
|
|
|
653
654
|
res = await checkpointer_client.get("/ok")
|
|
654
655
|
res.raise_for_status()
|
|
655
656
|
return True
|
|
656
|
-
except httpx.HTTPError:
|
|
657
|
-
|
|
657
|
+
except httpx.HTTPError as exc:
|
|
658
|
+
logger.warning(
|
|
659
|
+
"JS healthcheck failed. Either the JS server is not running or the event loop is blocked by a CPU-intensive task.",
|
|
660
|
+
error=exc,
|
|
661
|
+
)
|
|
662
|
+
raise HTTPException(
|
|
663
|
+
status_code=500,
|
|
664
|
+
detail="JS healthcheck failed. Either the JS server is not running or the event loop is blocked by a CPU-intensive task.",
|
|
665
|
+
) from exc
|