langgraph-api 0.4.1__py3-none-any.whl → 0.7.3__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 (135) hide show
  1. langgraph_api/__init__.py +1 -1
  2. langgraph_api/api/__init__.py +111 -51
  3. langgraph_api/api/a2a.py +1610 -0
  4. langgraph_api/api/assistants.py +212 -89
  5. langgraph_api/api/mcp.py +3 -3
  6. langgraph_api/api/meta.py +52 -28
  7. langgraph_api/api/openapi.py +27 -17
  8. langgraph_api/api/profile.py +108 -0
  9. langgraph_api/api/runs.py +342 -195
  10. langgraph_api/api/store.py +19 -2
  11. langgraph_api/api/threads.py +209 -27
  12. langgraph_api/asgi_transport.py +14 -9
  13. langgraph_api/asyncio.py +14 -4
  14. langgraph_api/auth/custom.py +52 -37
  15. langgraph_api/auth/langsmith/backend.py +4 -3
  16. langgraph_api/auth/langsmith/client.py +13 -8
  17. langgraph_api/cli.py +230 -133
  18. langgraph_api/command.py +5 -3
  19. langgraph_api/config/__init__.py +532 -0
  20. langgraph_api/config/_parse.py +58 -0
  21. langgraph_api/config/schemas.py +431 -0
  22. langgraph_api/cron_scheduler.py +17 -1
  23. langgraph_api/encryption/__init__.py +15 -0
  24. langgraph_api/encryption/aes_json.py +158 -0
  25. langgraph_api/encryption/context.py +35 -0
  26. langgraph_api/encryption/custom.py +280 -0
  27. langgraph_api/encryption/middleware.py +632 -0
  28. langgraph_api/encryption/shared.py +63 -0
  29. langgraph_api/errors.py +12 -1
  30. langgraph_api/executor_entrypoint.py +11 -6
  31. langgraph_api/feature_flags.py +29 -0
  32. langgraph_api/graph.py +176 -76
  33. langgraph_api/grpc/client.py +313 -0
  34. langgraph_api/grpc/config_conversion.py +231 -0
  35. langgraph_api/grpc/generated/__init__.py +29 -0
  36. langgraph_api/grpc/generated/checkpointer_pb2.py +63 -0
  37. langgraph_api/grpc/generated/checkpointer_pb2.pyi +99 -0
  38. langgraph_api/grpc/generated/checkpointer_pb2_grpc.py +329 -0
  39. langgraph_api/grpc/generated/core_api_pb2.py +216 -0
  40. langgraph_api/grpc/generated/core_api_pb2.pyi +905 -0
  41. langgraph_api/grpc/generated/core_api_pb2_grpc.py +1621 -0
  42. langgraph_api/grpc/generated/engine_common_pb2.py +219 -0
  43. langgraph_api/grpc/generated/engine_common_pb2.pyi +722 -0
  44. langgraph_api/grpc/generated/engine_common_pb2_grpc.py +24 -0
  45. langgraph_api/grpc/generated/enum_cancel_run_action_pb2.py +37 -0
  46. langgraph_api/grpc/generated/enum_cancel_run_action_pb2.pyi +12 -0
  47. langgraph_api/grpc/generated/enum_cancel_run_action_pb2_grpc.py +24 -0
  48. langgraph_api/grpc/generated/enum_control_signal_pb2.py +37 -0
  49. langgraph_api/grpc/generated/enum_control_signal_pb2.pyi +16 -0
  50. langgraph_api/grpc/generated/enum_control_signal_pb2_grpc.py +24 -0
  51. langgraph_api/grpc/generated/enum_durability_pb2.py +37 -0
  52. langgraph_api/grpc/generated/enum_durability_pb2.pyi +16 -0
  53. langgraph_api/grpc/generated/enum_durability_pb2_grpc.py +24 -0
  54. langgraph_api/grpc/generated/enum_multitask_strategy_pb2.py +37 -0
  55. langgraph_api/grpc/generated/enum_multitask_strategy_pb2.pyi +16 -0
  56. langgraph_api/grpc/generated/enum_multitask_strategy_pb2_grpc.py +24 -0
  57. langgraph_api/grpc/generated/enum_run_status_pb2.py +37 -0
  58. langgraph_api/grpc/generated/enum_run_status_pb2.pyi +22 -0
  59. langgraph_api/grpc/generated/enum_run_status_pb2_grpc.py +24 -0
  60. langgraph_api/grpc/generated/enum_stream_mode_pb2.py +37 -0
  61. langgraph_api/grpc/generated/enum_stream_mode_pb2.pyi +28 -0
  62. langgraph_api/grpc/generated/enum_stream_mode_pb2_grpc.py +24 -0
  63. langgraph_api/grpc/generated/enum_thread_status_pb2.py +37 -0
  64. langgraph_api/grpc/generated/enum_thread_status_pb2.pyi +16 -0
  65. langgraph_api/grpc/generated/enum_thread_status_pb2_grpc.py +24 -0
  66. langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.py +37 -0
  67. langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.pyi +16 -0
  68. langgraph_api/grpc/generated/enum_thread_stream_mode_pb2_grpc.py +24 -0
  69. langgraph_api/grpc/generated/errors_pb2.py +39 -0
  70. langgraph_api/grpc/generated/errors_pb2.pyi +21 -0
  71. langgraph_api/grpc/generated/errors_pb2_grpc.py +24 -0
  72. langgraph_api/grpc/ops/__init__.py +370 -0
  73. langgraph_api/grpc/ops/assistants.py +424 -0
  74. langgraph_api/grpc/ops/runs.py +792 -0
  75. langgraph_api/grpc/ops/threads.py +1013 -0
  76. langgraph_api/http.py +16 -5
  77. langgraph_api/http_metrics.py +15 -35
  78. langgraph_api/http_metrics_utils.py +38 -0
  79. langgraph_api/js/build.mts +1 -1
  80. langgraph_api/js/client.http.mts +13 -7
  81. langgraph_api/js/client.mts +2 -5
  82. langgraph_api/js/package.json +29 -28
  83. langgraph_api/js/remote.py +56 -30
  84. langgraph_api/js/src/graph.mts +20 -0
  85. langgraph_api/js/sse.py +2 -2
  86. langgraph_api/js/ui.py +1 -1
  87. langgraph_api/js/yarn.lock +1204 -1006
  88. langgraph_api/logging.py +29 -2
  89. langgraph_api/metadata.py +99 -28
  90. langgraph_api/middleware/http_logger.py +7 -2
  91. langgraph_api/middleware/private_network.py +7 -7
  92. langgraph_api/models/run.py +54 -93
  93. langgraph_api/otel_context.py +205 -0
  94. langgraph_api/patch.py +5 -3
  95. langgraph_api/queue_entrypoint.py +154 -65
  96. langgraph_api/route.py +47 -5
  97. langgraph_api/schema.py +88 -10
  98. langgraph_api/self_hosted_logs.py +124 -0
  99. langgraph_api/self_hosted_metrics.py +450 -0
  100. langgraph_api/serde.py +79 -37
  101. langgraph_api/server.py +138 -60
  102. langgraph_api/state.py +4 -3
  103. langgraph_api/store.py +25 -16
  104. langgraph_api/stream.py +80 -29
  105. langgraph_api/thread_ttl.py +31 -13
  106. langgraph_api/timing/__init__.py +25 -0
  107. langgraph_api/timing/profiler.py +200 -0
  108. langgraph_api/timing/timer.py +318 -0
  109. langgraph_api/utils/__init__.py +53 -8
  110. langgraph_api/utils/cache.py +47 -10
  111. langgraph_api/utils/config.py +2 -1
  112. langgraph_api/utils/errors.py +77 -0
  113. langgraph_api/utils/future.py +10 -6
  114. langgraph_api/utils/headers.py +76 -2
  115. langgraph_api/utils/retriable_client.py +74 -0
  116. langgraph_api/utils/stream_codec.py +315 -0
  117. langgraph_api/utils/uuids.py +29 -62
  118. langgraph_api/validation.py +9 -0
  119. langgraph_api/webhook.py +120 -6
  120. langgraph_api/worker.py +55 -24
  121. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/METADATA +16 -8
  122. langgraph_api-0.7.3.dist-info/RECORD +168 -0
  123. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/WHEEL +1 -1
  124. langgraph_runtime/__init__.py +1 -0
  125. langgraph_runtime/routes.py +11 -0
  126. logging.json +1 -3
  127. openapi.json +839 -478
  128. langgraph_api/config.py +0 -387
  129. langgraph_api/js/isolate-0x130008000-46649-46649-v8.log +0 -4430
  130. langgraph_api/js/isolate-0x138008000-44681-44681-v8.log +0 -4430
  131. langgraph_api/js/package-lock.json +0 -3308
  132. langgraph_api-0.4.1.dist-info/RECORD +0 -107
  133. /langgraph_api/{utils.py → grpc/__init__.py} +0 -0
  134. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/entry_points.txt +0 -0
  135. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/licenses/LICENSE +0 -0
langgraph_api/errors.py CHANGED
@@ -17,8 +17,19 @@ async def http_exception_handler(request: Request, exc: HTTPException) -> Respon
17
17
  headers = getattr(exc, "headers", None)
18
18
  if not is_body_allowed_for_status_code(exc.status_code):
19
19
  return Response(status_code=exc.status_code, headers=headers)
20
+
21
+ detail = exc.detail
22
+ if not detail or not isinstance(detail, str):
23
+ logger.warning(
24
+ "HTTPException detail is not a string or was not set",
25
+ detail_type=type(detail).__name__,
26
+ status_code=exc.status_code,
27
+ )
28
+ # Use safe fallback that won't fail or leak sensitive info
29
+ detail = "unknown error"
30
+
20
31
  return JSONResponse(
21
- {"detail": exc.detail}, status_code=exc.status_code, headers=headers
32
+ {"detail": detail}, status_code=exc.status_code, headers=headers
22
33
  )
23
34
 
24
35
 
@@ -4,13 +4,10 @@ import json
4
4
  import logging.config
5
5
  import pathlib
6
6
 
7
- from langgraph_api.queue_entrypoint import main
7
+ from langgraph_api.queue_entrypoint import main as queue_main
8
8
 
9
- if __name__ == "__main__":
10
- parser = argparse.ArgumentParser()
11
9
 
12
- parser.add_argument("--grpc-port", type=int, default=50051)
13
- args = parser.parse_args()
10
+ async def main(grpc_port: int = 50051):
14
11
  with open(pathlib.Path(__file__).parent.parent / "logging.json") as file:
15
12
  loaded_config = json.load(file)
16
13
  logging.config.dictConfig(loaded_config)
@@ -23,4 +20,12 @@ if __name__ == "__main__":
23
20
  from langgraph_api import config
24
21
 
25
22
  config.IS_EXECUTOR_ENTRYPOINT = True
26
- asyncio.run(main(grpc_port=args.grpc_port, entrypoint_name="python-executor"))
23
+ await queue_main(grpc_port=grpc_port, entrypoint_name="python-executor")
24
+
25
+
26
+ if __name__ == "__main__":
27
+ parser = argparse.ArgumentParser()
28
+
29
+ parser.add_argument("--grpc-port", type=int, default=50051)
30
+ args = parser.parse_args()
31
+ asyncio.run(main(grpc_port=args.grpc_port))
@@ -1,3 +1,5 @@
1
+ import os
2
+
1
3
  from langgraph.version import __version__
2
4
 
3
5
  # Only gate features on the major.minor version; Lets you ignore the rc/alpha/etc. releases anyway
@@ -6,3 +8,30 @@ LANGGRAPH_PY_MINOR = tuple(map(int, __version__.split(".")[:2]))
6
8
  OMIT_PENDING_SENDS = LANGGRAPH_PY_MINOR >= (0, 5)
7
9
  USE_RUNTIME_CONTEXT_API = LANGGRAPH_PY_MINOR >= (0, 6)
8
10
  USE_NEW_INTERRUPTS = LANGGRAPH_PY_MINOR >= (0, 6)
11
+ USE_DURABILITY = LANGGRAPH_PY_MINOR >= (0, 6)
12
+
13
+ # Feature flag for new gRPC-based persistence layer
14
+ FF_USE_CORE_API = os.getenv("FF_USE_CORE_API", "false").lower() in (
15
+ "true",
16
+ "1",
17
+ "yes",
18
+ )
19
+
20
+ # Runtime edition detection
21
+ _RUNTIME_EDITION = os.getenv("LANGGRAPH_RUNTIME_EDITION", "inmem")
22
+ IS_POSTGRES_BACKEND = _RUNTIME_EDITION == "postgres"
23
+ IS_POSTGRES_OR_GRPC_BACKEND = IS_POSTGRES_BACKEND or FF_USE_CORE_API
24
+ # Feature flag for using the JS native API
25
+ FF_USE_JS_API = os.getenv("FF_USE_JS_API", "false").lower() in (
26
+ "true",
27
+ "1",
28
+ "yes",
29
+ )
30
+
31
+ # In langgraph <= 1.0.3, we automatically subscribed to updates stream events to surface interrupts. In langgraph 1.0.4 we include interrupts in values events (which we are automatically subscribed to), so we no longer need to implicitly subscribe to updates stream events
32
+ # If the version is not valid, e.g. rc/alpha/etc., we default to 0.0.0
33
+ try:
34
+ LANGGRAPH_PY_PATCH = tuple(map(int, __version__.split(".")[:3]))
35
+ except ValueError:
36
+ LANGGRAPH_PY_PATCH = (0, 0, 0)
37
+ UPDATES_NEEDED_FOR_INTERRUPTS = LANGGRAPH_PY_PATCH <= (1, 0, 3)
langgraph_api/graph.py CHANGED
@@ -3,17 +3,20 @@ import functools
3
3
  import glob
4
4
  import importlib.util
5
5
  import inspect
6
+ import logging
6
7
  import os
7
8
  import sys
9
+ import time
8
10
  import warnings
9
11
  from collections.abc import AsyncIterator, Callable
10
12
  from contextlib import asynccontextmanager
11
13
  from itertools import filterfalse
12
- from typing import TYPE_CHECKING, Any, NamedTuple, TypeGuard, cast
14
+ from typing import Any, NamedTuple, TypeGuard, cast
13
15
  from uuid import UUID, uuid5
14
16
 
15
17
  import orjson
16
18
  import structlog
19
+ from langchain_core.embeddings import Embeddings # noqa: TC002
17
20
  from langgraph.checkpoint.base import BaseCheckpointSaver
18
21
  from langgraph.constants import CONFIG_KEY_CHECKPOINTER
19
22
  from langgraph.graph import StateGraph
@@ -21,15 +24,17 @@ from langgraph.pregel import Pregel
21
24
  from langgraph.store.base import BaseStore
22
25
  from starlette.exceptions import HTTPException
23
26
 
24
- from langgraph_api import asyncio as lg_asyncio
25
- from langgraph_api import config
26
- from langgraph_api.feature_flags import USE_RUNTIME_CONTEXT_API
27
+ from langgraph_api import config as lg_api_config
28
+ from langgraph_api import timing
29
+ from langgraph_api.feature_flags import (
30
+ IS_POSTGRES_OR_GRPC_BACKEND,
31
+ USE_RUNTIME_CONTEXT_API,
32
+ )
27
33
  from langgraph_api.js.base import BaseRemotePregel, is_js_path
28
34
  from langgraph_api.schema import Config
35
+ from langgraph_api.timing import profiled_import
29
36
  from langgraph_api.utils.config import run_in_executor, var_child_runnable_config
30
-
31
- if TYPE_CHECKING:
32
- from langchain_core.embeddings import Embeddings
37
+ from langgraph_api.utils.errors import GraphLoadError
33
38
 
34
39
  logger = structlog.stdlib.get_logger(__name__)
35
40
 
@@ -52,9 +57,12 @@ async def register_graph(
52
57
  ) -> None:
53
58
  """Register a graph."""
54
59
  from langgraph_runtime.database import connect
55
- from langgraph_runtime.ops import Assistants
56
60
 
57
- await logger.ainfo(f"Registering graph with id '{graph_id}'", graph_id=graph_id)
61
+ if IS_POSTGRES_OR_GRPC_BACKEND:
62
+ from langgraph_api.grpc.ops import Assistants
63
+ else:
64
+ from langgraph_runtime.ops import Assistants
65
+
58
66
  GRAPHS[graph_id] = graph
59
67
  if callable(graph):
60
68
  FACTORY_ACCEPTS_CONFIG[graph_id] = len(inspect.signature(graph).parameters) > 0
@@ -84,28 +92,74 @@ async def register_graph(
84
92
  description=description,
85
93
  )
86
94
 
87
- await register_graph_db()
95
+ if not lg_api_config.IS_EXECUTOR_ENTRYPOINT:
96
+ await register_graph_db()
88
97
 
89
98
 
90
- def register_graph_sync(
91
- graph_id: str, graph: GraphValue, config: dict | None = None
99
+ def _validate_assistant_id(assistant_id: str) -> None:
100
+ """Validate an assistant ID is either a graph_id or a valid UUID. Throw an error if not valid."""
101
+ if assistant_id and assistant_id not in GRAPHS:
102
+ # Not a graph_id, must be a valid UUID
103
+ try:
104
+ UUID(assistant_id)
105
+ except ValueError:
106
+ # Invalid format - return 404 to match test expectations
107
+ raise HTTPException(
108
+ status_code=404,
109
+ detail=f"Assistant '{assistant_id}' not found",
110
+ ) from None
111
+
112
+
113
+ def _log_slow_graph_generation(
114
+ start: float,
115
+ value_type: str,
116
+ graph_id: str,
117
+ warn_threshold_ms: float = 100,
118
+ error_threshold_ms: float = 250,
92
119
  ) -> None:
93
- lg_asyncio.run_coroutine_threadsafe(register_graph(graph_id, graph, config))
120
+ """Log warning/error if graph generation was slow."""
121
+ elapsed_secs = time.perf_counter() - start
122
+ elapsed_ms = elapsed_secs * 1000
123
+ elapsed_ms_rounded = round(elapsed_ms, 2)
124
+ log_level = None
125
+ if elapsed_ms > error_threshold_ms:
126
+ log_level = logging.ERROR
127
+ elif elapsed_ms > warn_threshold_ms:
128
+ log_level = logging.WARNING
129
+ if log_level is not None:
130
+ logger.log(
131
+ log_level,
132
+ f"Slow graph load. Accessing graph '{graph_id}' took {elapsed_ms_rounded}ms."
133
+ " Move expensive initialization (API clients, DB connections, model loading)"
134
+ " from graph factory if you are seeing API slowness.",
135
+ elapsed_ms=elapsed_ms_rounded,
136
+ value_type=value_type,
137
+ graph_id=graph_id,
138
+ )
94
139
 
95
140
 
96
141
  @asynccontextmanager
97
- async def _generate_graph(value: Any) -> AsyncIterator[Any]:
98
- """Yield a graph object regardless of its type."""
142
+ async def _generate_graph(value: Any, graph_id: str) -> AsyncIterator[Any]:
143
+ """Yield a graph object regardless of its type.
144
+
145
+ Logs a warning if graph generation takes >100ms, error if >250ms.
146
+ """
147
+ start = time.perf_counter()
148
+ value_type = type(value).__name__
99
149
  if isinstance(value, Pregel | BaseRemotePregel):
100
150
  yield value
101
151
  elif hasattr(value, "__aenter__") and hasattr(value, "__aexit__"):
102
152
  async with value as ctx_value:
153
+ _log_slow_graph_generation(start, value_type, graph_id)
103
154
  yield ctx_value
104
155
  elif hasattr(value, "__enter__") and hasattr(value, "__exit__"):
105
156
  with value as ctx_value:
157
+ _log_slow_graph_generation(start, value_type, graph_id)
106
158
  yield ctx_value
107
159
  elif asyncio.iscoroutine(value):
108
- yield await value
160
+ result = await value
161
+ _log_slow_graph_generation(start, value_type, graph_id)
162
+ yield result
109
163
  else:
110
164
  yield value
111
165
 
@@ -134,14 +188,18 @@ async def get_graph(
134
188
  *,
135
189
  checkpointer: BaseCheckpointSaver | None = None,
136
190
  store: BaseStore | None = None,
191
+ is_for_execution: bool = True,
137
192
  ) -> AsyncIterator[Pregel]:
138
193
  """Return the runnable."""
139
194
  from langgraph_api.utils import config as lg_config
195
+ from langgraph_api.utils import merge_auth
140
196
 
141
197
  assert_graph_exists(graph_id)
142
198
  value = GRAPHS[graph_id]
143
199
  if is_factory(value, graph_id):
144
200
  config = lg_config.ensure_config(config)
201
+ config["configurable"]["__is_for_execution__"] = is_for_execution
202
+ config = merge_auth(config)
145
203
 
146
204
  if store is not None:
147
205
  if USE_RUNTIME_CONTEXT_API:
@@ -154,7 +212,7 @@ async def get_graph(
154
212
  elif isinstance(runtime, dict):
155
213
  patched_runtime = Runtime(**(runtime | {"store": store}))
156
214
  elif runtime.store is None:
157
- patched_runtime = cast(Runtime, runtime).override(store=store)
215
+ patched_runtime = cast("Runtime", runtime).override(store=store)
158
216
  else:
159
217
  patched_runtime = runtime
160
218
 
@@ -172,7 +230,7 @@ async def get_graph(
172
230
  var_child_runnable_config.set(config)
173
231
  value = value(config) if factory_accepts_config(value, graph_id) else value()
174
232
  try:
175
- async with _generate_graph(value) as graph_obj:
233
+ async with _generate_graph(value, graph_id) as graph_obj:
176
234
  if isinstance(graph_obj, StateGraph):
177
235
  graph_obj = graph_obj.compile()
178
236
  if not isinstance(graph_obj, Pregel | BaseRemotePregel):
@@ -234,9 +292,9 @@ class GraphSpec(NamedTuple):
234
292
  variable: str | None = None
235
293
  config: dict | None = None
236
294
  """The configuration for the graph.
237
-
295
+
238
296
  Contains information such as: tags, recursion_limit and configurable.
239
-
297
+
240
298
  Configurable is a dict containing user defined values for the graph.
241
299
  """
242
300
  description: str | None = None
@@ -361,7 +419,7 @@ async def collect_graphs_from_env(register: bool = False) -> None:
361
419
  py_specs = list(filterfalse(is_js_spec, specs))
362
420
 
363
421
  if js_specs:
364
- if config.API_VARIANT == "local_dev":
422
+ if lg_api_config.API_VARIANT == "local_dev":
365
423
  raise NotImplementedError(
366
424
  "LangGraph.JS graphs are not yet supported in local development mode. "
367
425
  "To run your JS graphs, either use the LangGraph Studio application "
@@ -391,15 +449,15 @@ async def collect_graphs_from_env(register: bool = False) -> None:
391
449
  )
392
450
 
393
451
  if (
394
- config.HTTP_CONFIG
395
- and config.HTTP_CONFIG.get("app")
396
- and is_js_path(config.HTTP_CONFIG.get("app").split(":")[0])
452
+ lg_api_config.HTTP_CONFIG
453
+ and (js_app := lg_api_config.HTTP_CONFIG.get("app"))
454
+ and is_js_path(js_app.split(":")[0])
397
455
  ):
398
456
  js_bg_tasks.add(
399
457
  asyncio.create_task(
400
458
  run_js_http_process(
401
459
  paths_str,
402
- config.HTTP_CONFIG.get("app"),
460
+ lg_api_config.HTTP_CONFIG or {},
403
461
  watch="--reload" in sys.argv[1:],
404
462
  ),
405
463
  )
@@ -418,7 +476,10 @@ async def collect_graphs_from_env(register: bool = False) -> None:
418
476
  )
419
477
 
420
478
  for spec in py_specs:
421
- graph = await run_in_executor(None, _graph_from_spec, spec)
479
+ try:
480
+ graph = await run_in_executor(None, _graph_from_spec, spec)
481
+ except Exception as exc:
482
+ raise GraphLoadError(spec, exc) from exc
422
483
  if register:
423
484
  await register_graph(
424
485
  spec.id, graph, spec.config, description=spec.description
@@ -428,7 +489,7 @@ async def collect_graphs_from_env(register: bool = False) -> None:
428
489
  def _handle_exception(task: asyncio.Task) -> None:
429
490
  try:
430
491
  task.result()
431
- except asyncio.CancelledError:
492
+ except (asyncio.CancelledError, SystemExit):
432
493
  pass
433
494
  except Exception as e:
434
495
  logger.exception("Task failed", exc_info=e)
@@ -447,42 +508,59 @@ def verify_graphs() -> None:
447
508
  asyncio.run(collect_graphs_from_env())
448
509
 
449
510
 
511
+ def _metadata_fn(spec: GraphSpec) -> dict[str, Any]:
512
+ return {"graph_id": spec.id, "module": spec.module, "path": spec.path}
513
+
514
+
515
+ @timing.timer(
516
+ message="Importing graph with id {graph_id}",
517
+ metadata_fn=_metadata_fn,
518
+ warn_threshold_secs=3,
519
+ warn_message=(
520
+ "Import for graph {graph_id} exceeded the expected startup time. "
521
+ "Slow initialization (often due to work executed at import time) can delay readiness, "
522
+ "reduce scale-out capacity, and may cause deployments to be marked unhealthy."
523
+ ),
524
+ error_threshold_secs=30,
525
+ )
450
526
  def _graph_from_spec(spec: GraphSpec) -> GraphValue:
451
527
  """Return a graph from a spec."""
452
528
  # import the graph module
453
- if spec.module:
454
- module = importlib.import_module(spec.module)
455
- elif spec.path:
456
- try:
457
- modname = (
458
- spec.path.replace("/", "__")
459
- .replace(".py", "")
460
- .replace(" ", "_")
461
- .lstrip(".")
462
- )
463
- modspec = importlib.util.spec_from_file_location(modname, spec.path)
464
- if modspec is None:
465
- raise ValueError(f"Could not find python file for graph: {spec}")
466
- module = importlib.util.module_from_spec(modspec)
467
- sys.modules[modname] = module
468
- modspec.loader.exec_module(module) # type: ignore[possibly-unbound-attribute]
469
- except ImportError as e:
470
- e.add_note(f"Could not import python module for graph:\n{spec}")
471
- if config.API_VARIANT == "local_dev":
472
- e.add_note(
473
- "This error likely means you haven't installed your project and its dependencies yet. Before running the server, install your project:\n\n"
474
- "If you are using requirements.txt:\n"
475
- "python -m pip install -r requirements.txt\n\n"
476
- "If you are using pyproject.toml or setuptools:\n"
477
- "python -m pip install -e .\n\n"
478
- "Make sure to run this command from your project's root directory (where your setup.py or pyproject.toml is located)"
529
+ import_path = f"{spec.module or spec.path}:{spec.variable or '<auto>'}"
530
+ with profiled_import(import_path):
531
+ if spec.module:
532
+ module = importlib.import_module(spec.module)
533
+ elif spec.path:
534
+ try:
535
+ modname = (
536
+ spec.path.replace("/", "__")
537
+ .replace(".py", "")
538
+ .replace(" ", "_")
539
+ .lstrip(".")
479
540
  )
480
- raise
481
- except FileNotFoundError as e:
482
- e.add_note(f"Could not find python file for graph: {spec}")
483
- raise
484
- else:
485
- raise ValueError("Graph specification must have a path or module")
541
+ modspec = importlib.util.spec_from_file_location(modname, spec.path)
542
+ if modspec is None:
543
+ raise ValueError(f"Could not find python file for graph: {spec}")
544
+ module = importlib.util.module_from_spec(modspec)
545
+ sys.modules[modname] = module
546
+ modspec.loader.exec_module(module) # type: ignore[possibly-unbound-attribute]
547
+ except ImportError as e:
548
+ e.add_note(f"Could not import python module for graph:\n{spec}")
549
+ if lg_api_config.API_VARIANT == "local_dev":
550
+ e.add_note(
551
+ "This error likely means you haven't installed your project and its dependencies yet. Before running the server, install your project:\n\n"
552
+ "If you are using requirements.txt:\n"
553
+ "python -m pip install -r requirements.txt\n\n"
554
+ "If you are using pyproject.toml or setuptools:\n"
555
+ "python -m pip install -e .\n\n"
556
+ "Make sure to run this command from your project's root directory (where your setup.py or pyproject.toml is located)"
557
+ )
558
+ raise
559
+ except FileNotFoundError as e:
560
+ e.add_note(f"Could not find python file for graph: {spec}")
561
+ raise
562
+ else:
563
+ raise ValueError("Graph specification must have a path or module")
486
564
 
487
565
  if spec.variable:
488
566
  try:
@@ -528,7 +606,7 @@ def _graph_from_spec(spec: GraphSpec) -> GraphValue:
528
606
  elif isinstance(graph, Pregel):
529
607
  # We don't want to fail real deployments, but this will help folks catch unnecessary custom components
530
608
  # before they deploy
531
- if config.API_VARIANT == "local_dev":
609
+ if lg_api_config.API_VARIANT == "local_dev":
532
610
  has_checkpointer = isinstance(graph.checkpointer, BaseCheckpointSaver)
533
611
  has_store = isinstance(graph.store, BaseStore)
534
612
  if has_checkpointer or has_store:
@@ -588,6 +666,13 @@ def _get_init_embeddings() -> Callable[[str, ...], "Embeddings"] | None:
588
666
  return None
589
667
 
590
668
 
669
+ @timing.timer(
670
+ message="Loading embeddings {embeddings_path}",
671
+ metadata_fn=lambda index_config: {"embeddings_path": index_config.get("embed")},
672
+ warn_threshold_secs=5,
673
+ warn_message="Loading embeddings '{embeddings_path}' took longer than expected",
674
+ error_threshold_secs=10,
675
+ )
591
676
  def resolve_embeddings(index_config: dict) -> "Embeddings":
592
677
  """Return embeddings from config.
593
678
 
@@ -606,26 +691,41 @@ def resolve_embeddings(index_config: dict) -> "Embeddings":
606
691
  from langchain_core.embeddings import Embeddings
607
692
  from langgraph.store.base import ensure_embeddings
608
693
 
609
- embed: str = index_config["embed"]
694
+ embed = index_config["embed"]
695
+ if isinstance(embed, Embeddings):
696
+ return embed
697
+ if callable(embed):
698
+ return ensure_embeddings(embed)
699
+ if not isinstance(embed, str):
700
+ raise ValueError(
701
+ f"Embeddings config must be a string or callable, got: {type(embed).__name__}"
702
+ )
610
703
  if ".py:" in embed:
611
704
  module_name, function = embed.rsplit(":", 1)
612
705
  module_name = module_name.rstrip(":")
613
706
 
614
707
  try:
615
- if "/" in module_name:
616
- # Load from file path
617
- modname = (
618
- module_name.replace("/", "__").replace(".py", "").replace(" ", "_")
619
- )
620
- modspec = importlib.util.spec_from_file_location(modname, module_name)
621
- if modspec is None:
622
- raise ValueError(f"Could not find embeddings file: {module_name}")
623
- module = importlib.util.module_from_spec(modspec)
624
- sys.modules[modname] = module
625
- modspec.loader.exec_module(module) # type: ignore[possibly-unbound-attribute]
626
- else:
627
- # Load from Python module
628
- module = importlib.import_module(module_name)
708
+ with profiled_import(embed):
709
+ if "/" in module_name:
710
+ # Load from file path
711
+ modname = (
712
+ module_name.replace("/", "__")
713
+ .replace(".py", "")
714
+ .replace(" ", "_")
715
+ )
716
+ modspec = importlib.util.spec_from_file_location(
717
+ modname, module_name
718
+ )
719
+ if modspec is None:
720
+ raise ValueError(
721
+ f"Could not find embeddings file: {module_name}"
722
+ )
723
+ module = importlib.util.module_from_spec(modspec)
724
+ sys.modules[modname] = module
725
+ modspec.loader.exec_module(module) # type: ignore[possibly-unbound-attribute]
726
+ else:
727
+ # Load from Python module
728
+ module = importlib.import_module(module_name)
629
729
 
630
730
  embedding_fn = getattr(module, function, None)
631
731
  if embedding_fn is None:
@@ -644,7 +744,7 @@ def resolve_embeddings(index_config: dict) -> "Embeddings":
644
744
 
645
745
  except ImportError as e:
646
746
  e.add_note(f"Could not import embeddings module:\n{module_name}\n\n")
647
- if config.API_VARIANT == "local_dev":
747
+ if lg_api_config.API_VARIANT == "local_dev":
648
748
  e.add_note(
649
749
  "If you're in development mode, make sure you've installed your project "
650
750
  "and its dependencies:\n"