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.
- langgraph_api/__init__.py +1 -1
- langgraph_api/api/__init__.py +111 -51
- langgraph_api/api/a2a.py +1610 -0
- langgraph_api/api/assistants.py +212 -89
- langgraph_api/api/mcp.py +3 -3
- langgraph_api/api/meta.py +52 -28
- langgraph_api/api/openapi.py +27 -17
- langgraph_api/api/profile.py +108 -0
- langgraph_api/api/runs.py +342 -195
- langgraph_api/api/store.py +19 -2
- langgraph_api/api/threads.py +209 -27
- langgraph_api/asgi_transport.py +14 -9
- langgraph_api/asyncio.py +14 -4
- langgraph_api/auth/custom.py +52 -37
- langgraph_api/auth/langsmith/backend.py +4 -3
- langgraph_api/auth/langsmith/client.py +13 -8
- langgraph_api/cli.py +230 -133
- langgraph_api/command.py +5 -3
- langgraph_api/config/__init__.py +532 -0
- langgraph_api/config/_parse.py +58 -0
- langgraph_api/config/schemas.py +431 -0
- langgraph_api/cron_scheduler.py +17 -1
- langgraph_api/encryption/__init__.py +15 -0
- langgraph_api/encryption/aes_json.py +158 -0
- langgraph_api/encryption/context.py +35 -0
- langgraph_api/encryption/custom.py +280 -0
- langgraph_api/encryption/middleware.py +632 -0
- langgraph_api/encryption/shared.py +63 -0
- langgraph_api/errors.py +12 -1
- langgraph_api/executor_entrypoint.py +11 -6
- langgraph_api/feature_flags.py +29 -0
- langgraph_api/graph.py +176 -76
- langgraph_api/grpc/client.py +313 -0
- langgraph_api/grpc/config_conversion.py +231 -0
- langgraph_api/grpc/generated/__init__.py +29 -0
- langgraph_api/grpc/generated/checkpointer_pb2.py +63 -0
- langgraph_api/grpc/generated/checkpointer_pb2.pyi +99 -0
- langgraph_api/grpc/generated/checkpointer_pb2_grpc.py +329 -0
- langgraph_api/grpc/generated/core_api_pb2.py +216 -0
- langgraph_api/grpc/generated/core_api_pb2.pyi +905 -0
- langgraph_api/grpc/generated/core_api_pb2_grpc.py +1621 -0
- langgraph_api/grpc/generated/engine_common_pb2.py +219 -0
- langgraph_api/grpc/generated/engine_common_pb2.pyi +722 -0
- langgraph_api/grpc/generated/engine_common_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_cancel_run_action_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_cancel_run_action_pb2.pyi +12 -0
- langgraph_api/grpc/generated/enum_cancel_run_action_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_control_signal_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_control_signal_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_control_signal_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_durability_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_durability_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_durability_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_multitask_strategy_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_multitask_strategy_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_multitask_strategy_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_run_status_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_run_status_pb2.pyi +22 -0
- langgraph_api/grpc/generated/enum_run_status_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_stream_mode_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_stream_mode_pb2.pyi +28 -0
- langgraph_api/grpc/generated/enum_stream_mode_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_thread_status_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_thread_status_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_thread_status_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_thread_stream_mode_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/errors_pb2.py +39 -0
- langgraph_api/grpc/generated/errors_pb2.pyi +21 -0
- langgraph_api/grpc/generated/errors_pb2_grpc.py +24 -0
- langgraph_api/grpc/ops/__init__.py +370 -0
- langgraph_api/grpc/ops/assistants.py +424 -0
- langgraph_api/grpc/ops/runs.py +792 -0
- langgraph_api/grpc/ops/threads.py +1013 -0
- langgraph_api/http.py +16 -5
- langgraph_api/http_metrics.py +15 -35
- langgraph_api/http_metrics_utils.py +38 -0
- langgraph_api/js/build.mts +1 -1
- langgraph_api/js/client.http.mts +13 -7
- langgraph_api/js/client.mts +2 -5
- langgraph_api/js/package.json +29 -28
- langgraph_api/js/remote.py +56 -30
- langgraph_api/js/src/graph.mts +20 -0
- langgraph_api/js/sse.py +2 -2
- langgraph_api/js/ui.py +1 -1
- langgraph_api/js/yarn.lock +1204 -1006
- langgraph_api/logging.py +29 -2
- langgraph_api/metadata.py +99 -28
- langgraph_api/middleware/http_logger.py +7 -2
- langgraph_api/middleware/private_network.py +7 -7
- langgraph_api/models/run.py +54 -93
- langgraph_api/otel_context.py +205 -0
- langgraph_api/patch.py +5 -3
- langgraph_api/queue_entrypoint.py +154 -65
- langgraph_api/route.py +47 -5
- langgraph_api/schema.py +88 -10
- langgraph_api/self_hosted_logs.py +124 -0
- langgraph_api/self_hosted_metrics.py +450 -0
- langgraph_api/serde.py +79 -37
- langgraph_api/server.py +138 -60
- langgraph_api/state.py +4 -3
- langgraph_api/store.py +25 -16
- langgraph_api/stream.py +80 -29
- langgraph_api/thread_ttl.py +31 -13
- langgraph_api/timing/__init__.py +25 -0
- langgraph_api/timing/profiler.py +200 -0
- langgraph_api/timing/timer.py +318 -0
- langgraph_api/utils/__init__.py +53 -8
- langgraph_api/utils/cache.py +47 -10
- langgraph_api/utils/config.py +2 -1
- langgraph_api/utils/errors.py +77 -0
- langgraph_api/utils/future.py +10 -6
- langgraph_api/utils/headers.py +76 -2
- langgraph_api/utils/retriable_client.py +74 -0
- langgraph_api/utils/stream_codec.py +315 -0
- langgraph_api/utils/uuids.py +29 -62
- langgraph_api/validation.py +9 -0
- langgraph_api/webhook.py +120 -6
- langgraph_api/worker.py +55 -24
- {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/METADATA +16 -8
- langgraph_api-0.7.3.dist-info/RECORD +168 -0
- {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/WHEEL +1 -1
- langgraph_runtime/__init__.py +1 -0
- langgraph_runtime/routes.py +11 -0
- logging.json +1 -3
- openapi.json +839 -478
- langgraph_api/config.py +0 -387
- langgraph_api/js/isolate-0x130008000-46649-46649-v8.log +0 -4430
- langgraph_api/js/isolate-0x138008000-44681-44681-v8.log +0 -4430
- langgraph_api/js/package-lock.json +0 -3308
- langgraph_api-0.4.1.dist-info/RECORD +0 -107
- /langgraph_api/{utils.py → grpc/__init__.py} +0 -0
- {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/entry_points.txt +0 -0
- {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":
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
langgraph_api/feature_flags.py
CHANGED
|
@@ -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
|
|
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
|
|
25
|
-
from langgraph_api import
|
|
26
|
-
from langgraph_api.feature_flags import
|
|
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
|
-
|
|
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
|
-
|
|
95
|
+
if not lg_api_config.IS_EXECUTOR_ENTRYPOINT:
|
|
96
|
+
await register_graph_db()
|
|
88
97
|
|
|
89
98
|
|
|
90
|
-
def
|
|
91
|
-
graph_id
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
395
|
-
and
|
|
396
|
-
and is_js_path(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
|
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"
|