langgraph-api 0.2.130__py3-none-any.whl → 0.2.134__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.
Files changed (51) hide show
  1. langgraph_api/__init__.py +1 -1
  2. langgraph_api/api/assistants.py +32 -6
  3. langgraph_api/api/meta.py +3 -1
  4. langgraph_api/api/openapi.py +1 -1
  5. langgraph_api/api/runs.py +50 -10
  6. langgraph_api/api/threads.py +27 -1
  7. langgraph_api/api/ui.py +2 -0
  8. langgraph_api/asgi_transport.py +2 -2
  9. langgraph_api/asyncio.py +10 -8
  10. langgraph_api/auth/custom.py +9 -4
  11. langgraph_api/auth/langsmith/client.py +1 -1
  12. langgraph_api/cli.py +5 -4
  13. langgraph_api/config.py +1 -1
  14. langgraph_api/executor_entrypoint.py +23 -0
  15. langgraph_api/graph.py +25 -9
  16. langgraph_api/http.py +10 -7
  17. langgraph_api/http_metrics.py +4 -1
  18. langgraph_api/js/build.mts +11 -2
  19. langgraph_api/js/client.http.mts +2 -0
  20. langgraph_api/js/client.mts +13 -3
  21. langgraph_api/js/package.json +2 -2
  22. langgraph_api/js/remote.py +17 -12
  23. langgraph_api/js/src/preload.mjs +9 -1
  24. langgraph_api/js/src/utils/files.mts +5 -2
  25. langgraph_api/js/sse.py +1 -1
  26. langgraph_api/js/yarn.lock +9 -9
  27. langgraph_api/logging.py +3 -3
  28. langgraph_api/middleware/http_logger.py +2 -1
  29. langgraph_api/models/run.py +19 -14
  30. langgraph_api/patch.py +2 -2
  31. langgraph_api/queue_entrypoint.py +33 -18
  32. langgraph_api/schema.py +88 -4
  33. langgraph_api/serde.py +32 -5
  34. langgraph_api/server.py +5 -3
  35. langgraph_api/state.py +8 -8
  36. langgraph_api/store.py +1 -1
  37. langgraph_api/stream.py +33 -20
  38. langgraph_api/traceblock.py +1 -1
  39. langgraph_api/utils/__init__.py +40 -5
  40. langgraph_api/utils/config.py +13 -4
  41. langgraph_api/utils/future.py +1 -1
  42. langgraph_api/utils/uuids.py +87 -0
  43. langgraph_api/validation.py +9 -0
  44. langgraph_api/webhook.py +20 -20
  45. langgraph_api/worker.py +8 -5
  46. {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/METADATA +2 -2
  47. {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/RECORD +51 -49
  48. openapi.json +331 -1
  49. {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/WHEEL +0 -0
  50. {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/entry_points.txt +0 -0
  51. {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/licenses/LICENSE +0 -0
langgraph_api/http.py CHANGED
@@ -72,7 +72,7 @@ class JsonHttpClient:
72
72
 
73
73
 
74
74
  _http_client: JsonHttpClient
75
- _loopback_client: JsonHttpClient = None
75
+ _loopback_client: JsonHttpClient | None = None
76
76
 
77
77
 
78
78
  async def start_http_client() -> None:
@@ -113,16 +113,16 @@ def get_loopback_client() -> JsonHttpClient:
113
113
  return _loopback_client
114
114
 
115
115
 
116
- def is_retriable_error(exception: Exception) -> bool:
116
+ def is_retriable_error(exception: BaseException) -> bool:
117
117
  # httpx error hierarchy: https://www.python-httpx.org/exceptions/
118
118
  # Retry all timeout related errors
119
119
  if isinstance(exception, httpx.TimeoutException | httpx.NetworkError):
120
120
  return True
121
121
  # Seems to just apply to HttpStatusError but doesn't hurt to check all
122
122
  if isinstance(exception, httpx.HTTPError):
123
- return getattr(exception, "response", None) is not None and (
124
- exception.response.status_code >= 500
125
- or exception.response.status_code == 429
123
+ response = getattr(exception, "response", None)
124
+ return response is not None and (
125
+ response.status_code >= 500 or response.status_code == 429
126
126
  )
127
127
  return False
128
128
 
@@ -149,7 +149,7 @@ async def http_request(
149
149
  request_timeout: float | None = 30,
150
150
  raise_error: bool = True,
151
151
  client: JsonHttpClient | None = None,
152
- ) -> httpx.Response:
152
+ ) -> None:
153
153
  """Make an HTTP request with retries.
154
154
 
155
155
  Args:
@@ -173,7 +173,10 @@ async def http_request(
173
173
 
174
174
  content = None
175
175
  if body is not None:
176
- content = body
176
+ if isinstance(body, str):
177
+ content = body.encode("utf-8")
178
+ else:
179
+ content = body
177
180
  elif json is not None:
178
181
  content = json_dumpb(json)
179
182
 
@@ -98,7 +98,10 @@ class HTTPMetricsCollector:
98
98
  hist_data["count"] += 1
99
99
 
100
100
  def get_metrics(
101
- self, project_id: str, revision_id: str, format: str = "prometheus"
101
+ self,
102
+ project_id: str | None,
103
+ revision_id: str | None,
104
+ format: str = "prometheus",
102
105
  ) -> dict | list[str]:
103
106
  if format == "json":
104
107
  return {
@@ -17,7 +17,14 @@ const __dirname = new URL(".", import.meta.url).pathname;
17
17
 
18
18
  async function main() {
19
19
  const specs = Object.entries(
20
- z.record(z.string()).parse(JSON.parse(process.env.LANGSERVE_GRAPHS)),
20
+ z
21
+ .record(
22
+ z.union([
23
+ z.string(),
24
+ z.object({ path: z.string(), description: z.string().nullish() }),
25
+ ]),
26
+ )
27
+ .parse(JSON.parse(process.env.LANGSERVE_GRAPHS)),
21
28
  ).filter(([_, spec]) => filterValidExportPath(spec));
22
29
 
23
30
  let GRAPH_SCHEMAS: Record<string, Record<string, GraphSchema> | false> = {};
@@ -49,7 +56,9 @@ async function main() {
49
56
  await Promise.all(
50
57
  specs.map(async ([graphId, rawSpec]) => {
51
58
  console.info(`[${graphId}]: Checking for source file existence`);
52
- const { resolved, ...spec } = await resolveGraph(rawSpec, {
59
+ const importPath =
60
+ typeof rawSpec === "string" ? rawSpec : rawSpec.path;
61
+ const { resolved, ...spec } = await resolveGraph(importPath, {
53
62
  onlyFilePresence: true,
54
63
  });
55
64
 
@@ -104,6 +104,8 @@ async function registerHttp(appPath: string, options: { cwd: string }) {
104
104
  }
105
105
 
106
106
  async function main() {
107
+ logger.info("Starting HTTP loop", { pid: process.pid });
108
+
107
109
  const http = z
108
110
  .object({
109
111
  app: z.string().optional(),
@@ -950,6 +950,8 @@ async function* getStateHistoryRequest(
950
950
  const __dirname = new URL(".", import.meta.url).pathname;
951
951
 
952
952
  async function main() {
953
+ logger.info("Starting graph loop", { pid: process.pid });
954
+
953
955
  const app = new Hono();
954
956
 
955
957
  GRAPH_OPTIONS = {
@@ -959,7 +961,12 @@ async function main() {
959
961
 
960
962
  const specs = Object.entries(
961
963
  z
962
- .record(z.string())
964
+ .record(
965
+ z.union([
966
+ z.string(),
967
+ z.object({ path: z.string(), description: z.string().nullish() }),
968
+ ]),
969
+ )
963
970
  .parse(JSON.parse(process.env.LANGSERVE_GRAPHS ?? "{}")),
964
971
  ).filter(([_, spec]) => filterValidExportPath(spec));
965
972
 
@@ -978,7 +985,8 @@ async function main() {
978
985
  await Promise.all(
979
986
  specs.map(async ([graphId, rawSpec]) => {
980
987
  logger.info(`Resolving graph ${graphId}`);
981
- const { resolved, ...spec } = await resolveGraph(rawSpec);
988
+ const importPath = typeof rawSpec === "string" ? rawSpec : rawSpec.path;
989
+ const { resolved, ...spec } = await resolveGraph(importPath);
982
990
 
983
991
  GRAPH_RESOLVED[graphId] = resolved;
984
992
  GRAPH_SPEC[graphId] = spec;
@@ -1140,7 +1148,9 @@ async function getNodesExecutedRequest(
1140
1148
  ) {
1141
1149
  const value = nodesExecuted;
1142
1150
  nodesExecuted = 0;
1143
- logger.debug(`Returning ${value} nodes executed. Reset nodes executed to ${nodesExecuted}.`);
1151
+ logger.debug(
1152
+ `Returning ${value} nodes executed. Reset nodes executed to ${nodesExecuted}.`,
1153
+ );
1144
1154
  return { nodesExecuted: value };
1145
1155
  }
1146
1156
  patchFetch();
@@ -11,8 +11,8 @@
11
11
  "@hono/zod-validator": "^0.2.2",
12
12
  "@langchain/core": "^0.3.59",
13
13
  "@langchain/langgraph": "^0.2.65",
14
- "@langchain/langgraph-api": "~0.0.56",
15
- "@langchain/langgraph-ui": "~0.0.56",
14
+ "@langchain/langgraph-api": "~0.0.59",
15
+ "@langchain/langgraph-ui": "~0.0.59",
16
16
  "@langchain/langgraph-checkpoint": "~0.0.18",
17
17
  "@types/json-schema": "^7.0.15",
18
18
  "@typescript/vfs": "^1.6.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.
@@ -203,15 +203,15 @@
203
203
  zod "^3.25.32"
204
204
  zod-to-json-schema "^3.22.3"
205
205
 
206
- "@langchain/langgraph-api@~0.0.56":
207
- version "0.0.56"
208
- resolved "https://registry.yarnpkg.com/@langchain/langgraph-api/-/langgraph-api-0.0.56.tgz#9e0a6e5dd9af2f6ec259a32ca9081b19457a0825"
209
- integrity sha512-BhrbMKSc3f4CXXDXgvHR8mE2bsgD/G6dDzr+yDkEC7a9eeFI7C8bPhkGmegFJ+q7i6GWtqlQOYSCWHqQFZxynQ==
206
+ "@langchain/langgraph-api@~0.0.59":
207
+ version "0.0.59"
208
+ resolved "https://registry.yarnpkg.com/@langchain/langgraph-api/-/langgraph-api-0.0.59.tgz#a3d69b8cc68ceebd8a8d86b77abd03924ddcd02c"
209
+ integrity sha512-pUt3yKB2z1nsXdhqpRQgVefYg5MdJVsqH64gTNzSb/JYJBPIUj2h8XttRx9+3mHtVqCzvL2bb+HVIterNFOKtw==
210
210
  dependencies:
211
211
  "@babel/code-frame" "^7.26.2"
212
212
  "@hono/node-server" "^1.12.0"
213
213
  "@hono/zod-validator" "^0.2.2"
214
- "@langchain/langgraph-ui" "0.0.56"
214
+ "@langchain/langgraph-ui" "0.0.59"
215
215
  "@types/json-schema" "^7.0.15"
216
216
  "@typescript/vfs" "^1.6.0"
217
217
  dedent "^1.5.3"
@@ -256,10 +256,10 @@
256
256
  p-retry "4"
257
257
  uuid "^9.0.0"
258
258
 
259
- "@langchain/langgraph-ui@0.0.56", "@langchain/langgraph-ui@~0.0.56":
260
- version "0.0.56"
261
- resolved "https://registry.yarnpkg.com/@langchain/langgraph-ui/-/langgraph-ui-0.0.56.tgz#b566afa4ad940fc6bd942205a00b3afac7fd7363"
262
- integrity sha512-cRU+fMCz1NAOjRLcreNa64Gap89yio0p8baRigehtL1SNsXJEaZ2xdGjbnjaWIl8rTrriDak4oisx07whwcBjQ==
259
+ "@langchain/langgraph-ui@0.0.59", "@langchain/langgraph-ui@~0.0.59":
260
+ version "0.0.59"
261
+ resolved "https://registry.yarnpkg.com/@langchain/langgraph-ui/-/langgraph-ui-0.0.59.tgz#41988eae48c7520c5ebfa9bcdda65ddadfe1aab9"
262
+ integrity sha512-x6Jqt7TZRfHGU0MyxVbz7ugucT/oJQm9kM+DlGdmfAEm2JrNqgy1W85E589xJCJjpuRiDLpP7NiqSwnp8lCEqg==
263
263
  dependencies:
264
264
  "@commander-js/extra-typings" "^13.0.0"
265
265
  commander "^13.0.0"
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())