langgraph-api 0.2.130__py3-none-any.whl → 0.2.132__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.

Files changed (46) hide show
  1. langgraph_api/__init__.py +1 -1
  2. langgraph_api/api/assistants.py +6 -5
  3. langgraph_api/api/meta.py +3 -1
  4. langgraph_api/api/openapi.py +1 -1
  5. langgraph_api/api/runs.py +13 -10
  6. langgraph_api/api/ui.py +2 -0
  7. langgraph_api/asgi_transport.py +2 -2
  8. langgraph_api/asyncio.py +10 -8
  9. langgraph_api/auth/custom.py +9 -4
  10. langgraph_api/auth/langsmith/client.py +1 -1
  11. langgraph_api/cli.py +5 -4
  12. langgraph_api/config.py +1 -1
  13. langgraph_api/executor_entrypoint.py +23 -0
  14. langgraph_api/graph.py +25 -9
  15. langgraph_api/http.py +10 -7
  16. langgraph_api/http_metrics.py +4 -1
  17. langgraph_api/js/build.mts +11 -2
  18. langgraph_api/js/client.http.mts +2 -0
  19. langgraph_api/js/client.mts +13 -3
  20. langgraph_api/js/remote.py +17 -12
  21. langgraph_api/js/src/preload.mjs +9 -1
  22. langgraph_api/js/src/utils/files.mts +5 -2
  23. langgraph_api/js/sse.py +1 -1
  24. langgraph_api/logging.py +3 -3
  25. langgraph_api/middleware/http_logger.py +2 -1
  26. langgraph_api/models/run.py +19 -14
  27. langgraph_api/patch.py +2 -2
  28. langgraph_api/queue_entrypoint.py +33 -18
  29. langgraph_api/schema.py +20 -1
  30. langgraph_api/serde.py +32 -5
  31. langgraph_api/server.py +5 -3
  32. langgraph_api/state.py +8 -8
  33. langgraph_api/store.py +1 -1
  34. langgraph_api/stream.py +33 -20
  35. langgraph_api/traceblock.py +1 -1
  36. langgraph_api/utils/__init__.py +21 -5
  37. langgraph_api/utils/config.py +13 -4
  38. langgraph_api/utils/future.py +1 -1
  39. langgraph_api/utils/uuids.py +87 -0
  40. langgraph_api/webhook.py +20 -20
  41. langgraph_api/worker.py +8 -5
  42. {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.132.dist-info}/METADATA +1 -1
  43. {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.132.dist-info}/RECORD +46 -44
  44. {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.132.dist-info}/WHEEL +0 -0
  45. {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.132.dist-info}/entry_points.txt +0 -0
  46. {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.132.dist-info}/licenses/LICENSE +0 -0
@@ -106,6 +106,10 @@ async def _client_stream(method: str, data: dict[str, Any]):
106
106
  yield sse.data
107
107
 
108
108
 
109
+ class NoopModel(BaseModel):
110
+ pass
111
+
112
+
109
113
  async def _client_invoke(method: str, data: dict[str, Any]):
110
114
  graph_id = data.get("graph_id")
111
115
  res = await _client.post(
@@ -177,13 +181,10 @@ class RemotePregel(BaseRemotePregel):
177
181
  nodes: list[Any] = response.pop("nodes")
178
182
  edges: list[Any] = response.pop("edges")
179
183
 
180
- class NoopModel(BaseModel):
181
- pass
182
-
183
184
  return DrawableGraph(
184
185
  {
185
186
  data["id"]: Node(
186
- data["id"], data["id"], NoopModel(), data.get("metadata")
187
+ data["id"], data["id"], NoopModel, data.get("metadata")
187
188
  )
188
189
  for data in nodes
189
190
  },
@@ -244,9 +245,9 @@ class RemotePregel(BaseRemotePregel):
244
245
  )
245
246
  return tuple(result)
246
247
 
247
- return StateSnapshot(
248
+ return StateSnapshot( # type: ignore[missing-argument]
248
249
  item.get("values"),
249
- item.get("next"),
250
+ cast(tuple, item.get("next", ())),
250
251
  item.get("config"),
251
252
  item.get("metadata"),
252
253
  item.get("createdAt"),
@@ -352,7 +353,7 @@ class RemotePregel(BaseRemotePregel):
352
353
  return result["nodesExecuted"]
353
354
 
354
355
 
355
- async def run_js_process(paths_str: str, watch: bool = False):
356
+ async def run_js_process(paths_str: str | None, watch: bool = False):
356
357
  # check if tsx is available
357
358
  tsx_path = shutil.which("tsx")
358
359
  if tsx_path is None:
@@ -360,7 +361,7 @@ async def run_js_process(paths_str: str, watch: bool = False):
360
361
  "tsx not found in PATH. Please upgrade to latest LangGraph CLI to support running JS graphs."
361
362
  )
362
363
  attempt = 0
363
- while not asyncio.current_task().cancelled():
364
+ while (current_task := asyncio.current_task()) and not current_task.cancelled():
364
365
  client_file = os.path.join(os.path.dirname(__file__), "client.mts")
365
366
  client_preload_file = os.path.join(
366
367
  os.path.dirname(__file__), "src", "preload.mjs"
@@ -408,7 +409,9 @@ async def run_js_process(paths_str: str, watch: bool = False):
408
409
  attempt += 1
409
410
 
410
411
 
411
- async def run_js_http_process(paths_str: str, http_config: dict, watch: bool = False):
412
+ async def run_js_http_process(
413
+ paths_str: str | None, http_config: dict, watch: bool = False
414
+ ):
412
415
  # check if tsx is available
413
416
  tsx_path = shutil.which("tsx")
414
417
  if tsx_path is None:
@@ -417,7 +420,7 @@ async def run_js_http_process(paths_str: str, http_config: dict, watch: bool = F
417
420
  )
418
421
 
419
422
  attempt = 0
420
- while not asyncio.current_task().cancelled():
423
+ while (current_task := asyncio.current_task()) and not current_task.cancelled():
421
424
  client_file = os.path.join(os.path.dirname(__file__), "client.http.mts")
422
425
  args = ("tsx", "watch", client_file) if watch else ("tsx", client_file)
423
426
  pid = None
@@ -795,7 +798,9 @@ async def wait_until_js_ready():
795
798
  ) as checkpointer_client,
796
799
  ):
797
800
  attempt = 0
798
- while not asyncio.current_task().cancelled():
801
+ while (
802
+ current_task := asyncio.current_task()
803
+ ) and not current_task.cancelled():
799
804
  try:
800
805
  res = await graph_client.get("/ok")
801
806
  res.raise_for_status()
@@ -909,7 +914,7 @@ class CustomJsAuthBackend(AuthenticationBackend):
909
914
 
910
915
 
911
916
  async def handle_js_auth_event(
912
- ctx: Auth.types.AuthContext | None,
917
+ ctx: Auth.types.AuthContext,
913
918
  value: dict,
914
919
  ) -> Auth.types.FilterType | None:
915
920
  if hasattr(ctx.user, "dict") and callable(ctx.user.dict):
@@ -11,7 +11,15 @@ const cwd = process.cwd();
11
11
  // be working fine as well.
12
12
  const firstGraphFile =
13
13
  Object.values(graphs)
14
- .flatMap((i) => i.split(":").at(0))
14
+ .map((i) => {
15
+ if (typeof i === "string") {
16
+ return i.split(":").at(0);
17
+ } else if (i && typeof i === "object" && i.path) {
18
+ return i.path.split(":").at(0);
19
+ }
20
+ return null;
21
+ })
22
+ .filter(Boolean)
15
23
  .at(0) || "index.mts";
16
24
 
17
25
  // enforce API @langchain/langgraph resolution
@@ -1,4 +1,7 @@
1
- export function filterValidExportPath(path: string | undefined) {
1
+ export function filterValidExportPath(
2
+ path: string | { path: string } | undefined,
3
+ ) {
2
4
  if (!path) return false;
3
- return !path.split(":")[0].endsWith(".py");
5
+ const p = typeof path === "string" ? path : path.path;
6
+ return !p.split(":")[0].endsWith(".py");
4
7
  }
langgraph_api/js/sse.py CHANGED
@@ -91,7 +91,7 @@ class SSEDecoder:
91
91
 
92
92
  sse = StreamPart(
93
93
  event=self._event,
94
- data=orjson.loads(self._data) if self._data else None,
94
+ data=orjson.loads(self._data) if self._data else None, # type: ignore[invalid-argument-type]
95
95
  )
96
96
 
97
97
  # NOTE: as per the SSE spec, do not reset last_event_id.
langgraph_api/logging.py CHANGED
@@ -19,8 +19,8 @@ LOG_LEVEL = log_env("LOG_LEVEL", cast=str, default="INFO")
19
19
  logging.getLogger().setLevel(LOG_LEVEL.upper())
20
20
  logging.getLogger("psycopg").setLevel(logging.WARNING)
21
21
 
22
- worker_config: contextvars.ContextVar[dict[str, typing.Any] | None] = (
23
- contextvars.ContextVar("worker_config", default=None)
22
+ worker_config = contextvars.ContextVar[dict[str, typing.Any] | None](
23
+ "worker_config", default=None
24
24
  )
25
25
 
26
26
  # custom processors
@@ -165,6 +165,6 @@ if not structlog.is_configured():
165
165
  structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
166
166
  ],
167
167
  logger_factory=structlog.stdlib.LoggerFactory(),
168
- wrapper_class=structlog.stdlib.BoundLogger,
168
+ wrapper_class=structlog.stdlib.BoundLogger, # type: ignore[invalid-argument-type]
169
169
  cache_logger_on_first_use=True,
170
170
  )
@@ -84,6 +84,7 @@ class AccessLoggerMiddleware:
84
84
 
85
85
  if method and route and status:
86
86
  HTTP_METRICS_COLLECTOR.record_request(method, route, status, latency)
87
+ qs = scope.get("query_string")
87
88
  self.logger.log(
88
89
  _get_level(status),
89
90
  f"{method} {path} {status} {latency}ms",
@@ -93,7 +94,7 @@ class AccessLoggerMiddleware:
93
94
  latency_ms=latency,
94
95
  route=route,
95
96
  path_params=scope.get("path_params"),
96
- query_string=scope.get("query_string").decode(),
97
+ query_string=qs.decode() if qs else "",
97
98
  proto=scope.get("http_version"),
98
99
  req_header=_headers_to_dict(scope.get("headers")),
99
100
  res_header=_headers_to_dict(info["response"].get("headers")),
@@ -3,11 +3,11 @@ import time
3
3
  import urllib.parse
4
4
  import uuid
5
5
  from collections.abc import Mapping, Sequence
6
- from typing import Any, NamedTuple
6
+ from typing import Any, NamedTuple, cast
7
7
  from uuid import UUID
8
8
 
9
9
  import orjson
10
- from langgraph.checkpoint.base.id import uuid6
10
+ import structlog
11
11
  from starlette.authentication import BaseUser
12
12
  from starlette.exceptions import HTTPException
13
13
  from typing_extensions import TypedDict
@@ -27,7 +27,10 @@ from langgraph_api.schema import (
27
27
  )
28
28
  from langgraph_api.utils import AsyncConnectionProto, get_auth_ctx
29
29
  from langgraph_api.utils.headers import should_include_header
30
- from langgraph_runtime.ops import Runs, logger
30
+ from langgraph_api.utils.uuids import uuid7
31
+ from langgraph_runtime.ops import Runs
32
+
33
+ logger = structlog.stdlib.get_logger(__name__)
31
34
 
32
35
 
33
36
  class LangSmithTracer(TypedDict, total=False):
@@ -183,7 +186,7 @@ LANGSMITH_PROJECT = "langsmith-project"
183
186
  DEFAULT_RUN_HEADERS_EXCLUDE = {"x-api-key", "x-tenant-id", "x-service-key"}
184
187
 
185
188
 
186
- def get_configurable_headers(headers: dict[str, str]) -> dict[str, str]:
189
+ def get_configurable_headers(headers: Mapping[str, str]) -> dict[str, str]:
187
190
  """Extract headers that should be added to run configuration.
188
191
 
189
192
  This function handles special cases like langsmith-trace and baggage headers,
@@ -249,7 +252,7 @@ async def create_valid_run(
249
252
  request_id = headers.get("x-request-id") # Will be null in the crons scheduler.
250
253
  (
251
254
  assistant_id,
252
- thread_id,
255
+ thread_id_,
253
256
  checkpoint_id,
254
257
  run_id,
255
258
  ) = _get_ids(
@@ -258,7 +261,7 @@ async def create_valid_run(
258
261
  run_id=run_id,
259
262
  )
260
263
  if (
261
- thread_id is None
264
+ thread_id_ is None
262
265
  and (command := payload.get("command"))
263
266
  and command.get("resume")
264
267
  ):
@@ -266,7 +269,9 @@ async def create_valid_run(
266
269
  status_code=400,
267
270
  detail="You must provide a thread_id when resuming.",
268
271
  )
269
- temporary = thread_id is None and payload.get("on_completion", "delete") == "delete"
272
+ temporary = (
273
+ thread_id_ is None and payload.get("on_completion", "delete") == "delete"
274
+ )
270
275
  stream_resumable = payload.get("stream_resumable", False)
271
276
  stream_mode, multitask_strategy, prevent_insert_if_inflight = assign_defaults(
272
277
  payload
@@ -296,7 +301,7 @@ async def create_valid_run(
296
301
  configurable.update(get_configurable_headers(headers))
297
302
  ctx = get_auth_ctx()
298
303
  if ctx:
299
- user = ctx.user
304
+ user = cast(BaseUser | None, ctx.user)
300
305
  user_id = get_user_id(user)
301
306
  configurable["langgraph_auth_user"] = user
302
307
  configurable["langgraph_auth_user_id"] = user_id
@@ -336,7 +341,7 @@ async def create_valid_run(
336
341
  metadata=payload.get("metadata"),
337
342
  status="pending",
338
343
  user_id=user_id,
339
- thread_id=thread_id,
344
+ thread_id=thread_id_,
340
345
  run_id=run_id,
341
346
  multitask_strategy=multitask_strategy,
342
347
  prevent_insert_if_inflight=prevent_insert_if_inflight,
@@ -362,7 +367,7 @@ async def create_valid_run(
362
367
  logger.info(
363
368
  "Created run",
364
369
  run_id=str(run_id),
365
- thread_id=str(thread_id),
370
+ thread_id=str(thread_id_),
366
371
  assistant_id=str(assistant_id),
367
372
  multitask_strategy=multitask_strategy,
368
373
  stream_mode=stream_mode,
@@ -383,7 +388,7 @@ async def create_valid_run(
383
388
  await Runs.cancel(
384
389
  conn,
385
390
  [run["run_id"] for run in inflight_runs],
386
- thread_id=thread_id,
391
+ thread_id=thread_id_,
387
392
  action=multitask_strategy,
388
393
  )
389
394
  except HTTPException:
@@ -415,15 +420,15 @@ def _get_ids(
415
420
  assistant_id = get_assistant_id(payload["assistant_id"])
416
421
 
417
422
  # ensure UUID validity defaults
418
- assistant_id, thread_id, checkpoint_id = ensure_ids(
423
+ assistant_id, thread_id_, checkpoint_id = ensure_ids(
419
424
  assistant_id, thread_id, payload
420
425
  )
421
426
 
422
- run_id = run_id or uuid6()
427
+ run_id = run_id or uuid7()
423
428
 
424
429
  return _Ids(
425
430
  assistant_id,
426
- thread_id,
431
+ thread_id_,
427
432
  checkpoint_id,
428
433
  run_id,
429
434
  )
langgraph_api/patch.py CHANGED
@@ -41,8 +41,8 @@ async def StreamingResponse_stream_response(self, send: Send) -> None:
41
41
 
42
42
  # patch StreamingResponse.stream_response
43
43
 
44
- StreamingResponse.stream_response = StreamingResponse_stream_response
44
+ StreamingResponse.stream_response = StreamingResponse_stream_response # type: ignore[invalid-assignment]
45
45
 
46
46
  # patch Response.render
47
47
 
48
- Response.render = Response_render
48
+ Response.render = Response_render # type: ignore[invalid-assignment]
@@ -18,9 +18,9 @@ import os
18
18
  import pathlib
19
19
  import signal
20
20
  from contextlib import asynccontextmanager
21
+ from typing import cast
21
22
 
22
23
  import structlog
23
- import uvloop
24
24
 
25
25
  from langgraph_runtime.lifespan import lifespan
26
26
  from langgraph_runtime.metrics import get_metrics
@@ -36,21 +36,22 @@ async def health_and_metrics_server():
36
36
  class HealthAndMetricsHandler(http.server.SimpleHTTPRequestHandler):
37
37
  def log_message(self, format, *args):
38
38
  # Skip logging for /ok and /metrics endpoints
39
- if self.path in ["/ok", "/metrics"]:
39
+ if getattr(self, "path", None) in ["/ok", "/metrics"]:
40
40
  return
41
41
  # Log other requests normally
42
42
  super().log_message(format, *args)
43
43
 
44
44
  def do_GET(self):
45
- if self.path == "/ok":
45
+ path = getattr(self, "path", None)
46
+ if path == "/ok":
46
47
  self.send_response(200)
47
48
  self.send_header("Content-Type", "application/json")
48
49
  self.send_header("Content-Length", ok_len)
49
50
  self.end_headers()
50
51
  self.wfile.write(ok)
51
- elif self.path == "/metrics":
52
+ elif path == "/metrics":
52
53
  metrics = get_metrics()
53
- worker_metrics = metrics["workers"]
54
+ worker_metrics = cast(dict[str, int], metrics["workers"])
54
55
  workers_max = worker_metrics["max"]
55
56
  workers_active = worker_metrics["active"]
56
57
  workers_available = worker_metrics["available"]
@@ -93,20 +94,27 @@ async def health_and_metrics_server():
93
94
  httpd.shutdown()
94
95
 
95
96
 
96
- async def entrypoint():
97
+ async def entrypoint(
98
+ grpc_port: int | None = None, entrypoint_name: str = "python-queue"
99
+ ):
97
100
  from langgraph_api import logging as lg_logging
98
101
  from langgraph_api.api import user_router
99
102
 
100
- lg_logging.set_logging_context({"entrypoint": "python-queue"})
103
+ lg_logging.set_logging_context({"entrypoint": entrypoint_name})
101
104
  tasks: set[asyncio.Task] = set()
102
105
  tasks.add(asyncio.create_task(health_and_metrics_server()))
103
106
 
104
107
  original_lifespan = user_router.router.lifespan_context if user_router else None
105
108
 
106
109
  @asynccontextmanager
107
- async def combined_lifespan(app, with_cron_scheduler=False, taskset=None):
110
+ async def combined_lifespan(
111
+ app, with_cron_scheduler=False, grpc_port=None, taskset=None
112
+ ):
108
113
  async with lifespan(
109
- app, with_cron_scheduler=with_cron_scheduler, taskset=taskset
114
+ app,
115
+ with_cron_scheduler=with_cron_scheduler,
116
+ grpc_port=grpc_port,
117
+ taskset=taskset,
110
118
  ):
111
119
  if original_lifespan:
112
120
  async with original_lifespan(app):
@@ -114,11 +122,13 @@ async def entrypoint():
114
122
  else:
115
123
  yield
116
124
 
117
- async with combined_lifespan(None, with_cron_scheduler=False, taskset=tasks):
125
+ async with combined_lifespan(
126
+ None, with_cron_scheduler=False, grpc_port=grpc_port, taskset=tasks
127
+ ):
118
128
  await asyncio.gather(*tasks)
119
129
 
120
130
 
121
- async def main():
131
+ async def main(grpc_port: int | None = None, entrypoint_name: str = "python-queue"):
122
132
  """Run the queue entrypoint and shut down gracefully on SIGTERM/SIGINT."""
123
133
  loop = asyncio.get_running_loop()
124
134
  stop_event = asyncio.Event()
@@ -132,11 +142,9 @@ async def main():
132
142
  except (NotImplementedError, RuntimeError):
133
143
  signal.signal(signal.SIGTERM, lambda *_: _handle_signal())
134
144
 
135
- from langgraph_api import config
136
-
137
- config.IS_QUEUE_ENTRYPOINT = True
138
-
139
- entry_task = asyncio.create_task(entrypoint())
145
+ entry_task = asyncio.create_task(
146
+ entrypoint(grpc_port=grpc_port, entrypoint_name=entrypoint_name)
147
+ )
140
148
  await stop_event.wait()
141
149
 
142
150
  logger.warning("Cancelling queue entrypoint task")
@@ -146,10 +154,17 @@ async def main():
146
154
 
147
155
 
148
156
  if __name__ == "__main__":
157
+ from langgraph_api import config
158
+
159
+ config.IS_QUEUE_ENTRYPOINT = True
149
160
  with open(pathlib.Path(__file__).parent.parent / "logging.json") as file:
150
161
  loaded_config = json.load(file)
151
162
  logging.config.dictConfig(loaded_config)
163
+ try:
164
+ import uvloop # type: ignore[unresolved-import]
152
165
 
153
- uvloop.install()
154
-
166
+ uvloop.install()
167
+ except ImportError:
168
+ pass
169
+ # run the entrypoint
155
170
  asyncio.run(main())
langgraph_api/schema.py CHANGED
@@ -3,6 +3,7 @@ from datetime import datetime
3
3
  from typing import Any, Literal, Optional, TypeAlias
4
4
  from uuid import UUID
5
5
 
6
+ from langchain_core.runnables.config import RunnableConfig
6
7
  from typing_extensions import TypedDict
7
8
 
8
9
  from langgraph_api.serde import Fragment
@@ -157,6 +158,22 @@ class ThreadState(TypedDict):
157
158
  """The interrupts for this state."""
158
159
 
159
160
 
161
+ class RunKwargs(TypedDict):
162
+ config: RunnableConfig
163
+ context: dict[str, Any]
164
+ input: dict[str, Any] | None
165
+ command: dict[str, Any] | None
166
+ stream_mode: StreamMode
167
+ interrupt_before: Sequence[str] | str | None
168
+ interrupt_after: Sequence[str] | str | None
169
+ webhook: str | None
170
+ feedback_keys: Sequence[str] | None
171
+ temporary: bool
172
+ subgraphs: bool
173
+ resumable: bool
174
+ checkpoint_during: bool
175
+
176
+
160
177
  class Run(TypedDict):
161
178
  run_id: UUID
162
179
  """The ID of the run."""
@@ -172,7 +189,7 @@ class Run(TypedDict):
172
189
  """The status of the run. One of 'pending', 'error', 'success'."""
173
190
  metadata: Fragment
174
191
  """The run metadata."""
175
- kwargs: Fragment
192
+ kwargs: RunKwargs
176
193
  """The run kwargs."""
177
194
  multitask_strategy: MultitaskStrategy
178
195
  """Strategy to handle concurrent runs on the same thread."""
@@ -214,6 +231,8 @@ class Cron(TypedDict):
214
231
  """The next run date of the cron."""
215
232
  metadata: Fragment
216
233
  """The cron metadata."""
234
+ now: datetime
235
+ """The current time."""
217
236
 
218
237
 
219
238
  class ThreadUpdateResponse(TypedDict):
langgraph_api/serde.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import base64
2
3
  import re
3
4
  import uuid
4
5
  from base64 import b64encode
@@ -16,7 +17,7 @@ from ipaddress import (
16
17
  )
17
18
  from pathlib import Path
18
19
  from re import Pattern
19
- from typing import Any, NamedTuple
20
+ from typing import Any, NamedTuple, cast
20
21
  from zoneinfo import ZoneInfo
21
22
 
22
23
  import cloudpickle
@@ -46,10 +47,14 @@ def decimal_encoder(dec_value: Decimal) -> int | float:
46
47
  >>> decimal_encoder(Decimal("1"))
47
48
  1
48
49
  """
49
- if dec_value.as_tuple().exponent >= 0:
50
- return int(dec_value)
51
- else:
50
+ if (
51
+ # maps to float('nan') / float('inf') / float('-inf')
52
+ not dec_value.is_finite()
53
+ # or regular float
54
+ or cast(int, dec_value.as_tuple().exponent) < 0
55
+ ):
52
56
  return float(dec_value)
57
+ return int(dec_value)
53
58
 
54
59
 
55
60
  def default(obj):
@@ -142,7 +147,7 @@ def json_loads(content: bytes | Fragment | dict) -> Any:
142
147
  content = content.buf
143
148
  if isinstance(content, dict):
144
149
  return content
145
- return orjson.loads(content)
150
+ return orjson.loads(cast(bytes, content))
146
151
 
147
152
 
148
153
  async def ajson_loads(content: bytes | Fragment) -> Any:
@@ -170,3 +175,25 @@ class Serializer(JsonPlusSerializer):
170
175
  )
171
176
  return None
172
177
  return super().loads_typed(data)
178
+
179
+
180
+ mpack_keys = {"method", "value"}
181
+ SERIALIZER = Serializer()
182
+
183
+
184
+ # TODO: Make more performant (by removing)
185
+ async def reserialize_message(message: bytes) -> bytes:
186
+ # Stream messages from golang runtime are a byte dict of StreamChunks.
187
+ loaded = await ajson_loads(message)
188
+ converted = {}
189
+ for k, v in loaded.items():
190
+ if isinstance(v, dict) and v.keys() == mpack_keys:
191
+ if v["method"] == "missing":
192
+ converted[k] = v["value"] # oops
193
+ else:
194
+ converted[k] = SERIALIZER.loads_typed(
195
+ (v["method"], base64.b64decode(v["value"]))
196
+ )
197
+ else:
198
+ converted[k] = v
199
+ return json_dumpb(converted)
langgraph_api/server.py CHANGED
@@ -122,17 +122,19 @@ if user_router:
122
122
  # Merge routes
123
123
  app = user_router
124
124
 
125
- meta_route_paths = [route.path for route in meta_routes]
125
+ meta_route_paths = [
126
+ getattr(route, "path", None) for route in meta_routes if hasattr(route, "path")
127
+ ]
126
128
  custom_route_paths = [
127
129
  route.path
128
130
  for route in user_router.router.routes
129
- if route.path not in meta_route_paths
131
+ if hasattr(route, "path") and route.path not in meta_route_paths
130
132
  ]
131
133
  logger.info(f"Custom route paths: {custom_route_paths}")
132
134
 
133
135
  update_openapi_spec(app)
134
136
  for route in routes:
135
- if route.path in ("/docs", "/openapi.json"):
137
+ if getattr(route, "path", None) in ("/docs", "/openapi.json"):
136
138
  # Our handlers for these are inclusive of the custom routes and default API ones
137
139
  # Don't let these be shadowed
138
140
  app.router.routes.insert(0, route)
langgraph_api/state.py CHANGED
@@ -60,11 +60,11 @@ def patch_interrupt(
60
60
  return {"id": id, **interrupt.raw}
61
61
 
62
62
  if USE_NEW_INTERRUPTS:
63
- interrupt = Interrupt(**interrupt) if isinstance(interrupt, dict) else interrupt
63
+ interrupt = Interrupt(**interrupt) if isinstance(interrupt, dict) else interrupt # type: ignore[missing-argument]
64
64
 
65
65
  return {
66
- "id": interrupt.id,
67
- "value": interrupt.value,
66
+ "id": interrupt.id, # type: ignore[unresolved-attribute]
67
+ "value": interrupt.value, # type: ignore[unresolved-attribute]
68
68
  }
69
69
  else:
70
70
  if isinstance(interrupt, dict):
@@ -72,16 +72,16 @@ def patch_interrupt(
72
72
  # id is the new field we use for identification, also not supported on init for old versions
73
73
  interrupt.pop("interrupt_id", None)
74
74
  interrupt.pop("id", None)
75
- interrupt = Interrupt(**interrupt)
75
+ interrupt = Interrupt(**interrupt) # type: ignore[missing-argument]
76
76
 
77
77
  return {
78
78
  "id": interrupt.interrupt_id
79
79
  if hasattr(interrupt, "interrupt_id")
80
80
  else None,
81
- "value": interrupt.value,
82
- "resumable": interrupt.resumable,
83
- "ns": interrupt.ns,
84
- "when": interrupt.when,
81
+ "value": interrupt.value, # type: ignore[unresolved-attribute]
82
+ "resumable": interrupt.resumable, # type: ignore[unresolved-attribute]
83
+ "ns": interrupt.ns, # type: ignore[unresolved-attribute]
84
+ "when": interrupt.when, # type: ignore[unresolved-attribute]
85
85
  }
86
86
 
87
87
 
langgraph_api/store.py CHANGED
@@ -93,7 +93,7 @@ def _load_store(store_path: str) -> Any:
93
93
  raise ValueError(f"Could not find store file: {path_name}")
94
94
  module = importlib.util.module_from_spec(modspec)
95
95
  sys.modules[module_name] = module
96
- modspec.loader.exec_module(module)
96
+ modspec.loader.exec_module(module) # type: ignore[possibly-unbound-attribute]
97
97
 
98
98
  else:
99
99
  path_name, function = store_path.rsplit(".", 1)