langgraph-api 0.0.15__tar.gz → 0.0.17__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 (93) hide show
  1. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/PKG-INFO +2 -2
  2. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/api/assistants.py +49 -17
  3. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/api/store.py +67 -15
  4. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/asyncio.py +5 -0
  5. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/auth/custom.py +25 -6
  6. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/auth/noop.py +9 -1
  7. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/config.py +6 -1
  8. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/cron_scheduler.py +7 -4
  9. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/client.mts +5 -1
  10. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/client.new.mts +5 -1
  11. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/package.json +2 -2
  12. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/remote_new.py +10 -2
  13. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/remote_old.py +10 -2
  14. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/tests/api.test.mts +124 -7
  15. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/tests/graphs/agent.mts +58 -3
  16. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/tests/graphs/delay.mts +5 -0
  17. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/yarn.lock +13 -13
  18. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/models/run.py +9 -2
  19. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/queue.py +58 -8
  20. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/route.py +8 -3
  21. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/schema.py +1 -1
  22. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/stream.py +9 -2
  23. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/utils.py +14 -6
  24. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_storage/ops.py +9 -2
  25. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/openapi.json +5 -5
  26. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/pyproject.toml +2 -2
  27. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/LICENSE +0 -0
  28. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/README.md +0 -0
  29. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/__init__.py +0 -0
  30. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/api/__init__.py +0 -0
  31. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/api/meta.py +0 -0
  32. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/api/openapi.py +0 -0
  33. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/api/runs.py +0 -0
  34. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/api/threads.py +0 -0
  35. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/auth/__init__.py +0 -0
  36. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/auth/langsmith/__init__.py +0 -0
  37. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/auth/langsmith/backend.py +0 -0
  38. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/auth/langsmith/client.py +0 -0
  39. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/auth/middleware.py +0 -0
  40. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/auth/studio_user.py +0 -0
  41. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/cli.py +0 -0
  42. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/errors.py +0 -0
  43. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/graph.py +0 -0
  44. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/http.py +0 -0
  45. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/http_logger.py +0 -0
  46. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/.gitignore +0 -0
  47. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/base.py +0 -0
  48. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/build.mts +0 -0
  49. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/errors.py +0 -0
  50. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/global.d.ts +0 -0
  51. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/remote.py +0 -0
  52. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/schema.py +0 -0
  53. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/server_sent_events.py +0 -0
  54. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/src/graph.mts +0 -0
  55. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/src/hooks.mjs +0 -0
  56. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/src/parser/parser.mts +0 -0
  57. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/src/parser/parser.worker.mjs +0 -0
  58. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/src/schema/types.mts +0 -0
  59. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/src/schema/types.template.mts +0 -0
  60. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/src/utils/importMap.mts +0 -0
  61. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
  62. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/src/utils/serde.mts +0 -0
  63. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/tests/compose-postgres.yml +0 -0
  64. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/tests/graphs/.gitignore +0 -0
  65. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/tests/graphs/error.mts +0 -0
  66. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/tests/graphs/langgraph.json +0 -0
  67. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/tests/graphs/nested.mts +0 -0
  68. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/tests/graphs/package.json +0 -0
  69. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/tests/graphs/weather.mts +0 -0
  70. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/tests/graphs/yarn.lock +0 -0
  71. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/tests/parser.test.mts +0 -0
  72. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/js/tests/utils.mts +0 -0
  73. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/lifespan.py +0 -0
  74. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/logging.py +0 -0
  75. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/metadata.py +0 -0
  76. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/models/__init__.py +0 -0
  77. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/patch.py +0 -0
  78. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/serde.py +0 -0
  79. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/server.py +0 -0
  80. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/sse.py +0 -0
  81. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/state.py +0 -0
  82. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_api/validation.py +0 -0
  83. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_license/__init__.py +0 -0
  84. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_license/middleware.py +0 -0
  85. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_license/validation.py +0 -0
  86. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_storage/__init__.py +0 -0
  87. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_storage/checkpoint.py +0 -0
  88. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_storage/database.py +0 -0
  89. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_storage/queue.py +0 -0
  90. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_storage/retry.py +0 -0
  91. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_storage/store.py +0 -0
  92. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/langgraph_storage/ttl_dict.py +0 -0
  93. {langgraph_api-0.0.15 → langgraph_api-0.0.17}/logging.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langgraph-api
3
- Version: 0.0.15
3
+ Version: 0.0.17
4
4
  Summary:
5
5
  License: Elastic-2.0
6
6
  Author: Nuno Campos
@@ -16,7 +16,7 @@ Requires-Dist: jsonschema-rs (>=0.25.0,<0.26.0)
16
16
  Requires-Dist: langchain-core (>=0.2.38,<0.4.0)
17
17
  Requires-Dist: langgraph (>=0.2.56,<0.3.0)
18
18
  Requires-Dist: langgraph-checkpoint (>=2.0.7,<3.0)
19
- Requires-Dist: langgraph-sdk (>=0.1.48,<0.2.0)
19
+ Requires-Dist: langgraph-sdk (>=0.1.51,<0.2.0)
20
20
  Requires-Dist: langsmith (>=0.1.63,<0.3.0)
21
21
  Requires-Dist: orjson (>=3.10.1)
22
22
  Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
@@ -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
- return ApiResponse(graph.get_graph(xray=xray).to_json())
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
- return ApiResponse(
171
- {
172
- ns: _graph_schemas(subgraph)
173
- async for ns, subgraph in graph.aget_subgraphs(
174
- namespace=namespace,
175
- recurse=request.query_params.get("recurse", "False")
176
- in ("true", "True"),
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"],
@@ -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
- key = payload["key"]
32
- value = payload["value"]
33
- await Store().aput(namespace, key, value)
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
- key = payload["key"]
58
- await Store().adelete(namespace, key)
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
- namespace_prefix,
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=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
 
@@ -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
@@ -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), _normalize_user(user)
504
+ return AuthCredentials(permissions), normalize_user(user)
485
505
 
486
506
 
487
- def _normalize_user(user: Any) -> BaseUser:
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
- result = auth._handlers[key][
575
- -1
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:
@@ -2,11 +2,19 @@ from starlette.authentication import (
2
2
  AuthCredentials,
3
3
  AuthenticationBackend,
4
4
  BaseUser,
5
- UnauthenticatedUser,
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
@@ -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=False)
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)
@@ -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.utils import next_cron_date, with_user
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
- async with with_user(SimpleUser(str(cron["user_id"])), None):
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=(
@@ -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"> = new Set();
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
  }
@@ -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"> = new Set();
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
  }
@@ -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.31",
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.1.1",
25
+ "zeromq": "^6.3.0",
26
26
  "zod": "^3.23.8"
27
27
  },
28
28
  "devDependencies": {
@@ -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
- return False
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
@@ -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
- return False
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