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/api/assistants.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from functools import partial
|
|
1
2
|
from typing import Any
|
|
2
3
|
from uuid import uuid4
|
|
3
4
|
|
|
5
|
+
import jsonschema_rs
|
|
4
6
|
import structlog
|
|
5
7
|
|
|
6
8
|
# TODO: Remove dependency on langchain-core here.
|
|
@@ -12,18 +14,28 @@ from starlette.responses import Response
|
|
|
12
14
|
from starlette.routing import BaseRoute
|
|
13
15
|
|
|
14
16
|
from langgraph_api import store as api_store
|
|
15
|
-
from langgraph_api.
|
|
17
|
+
from langgraph_api.encryption.middleware import (
|
|
18
|
+
decrypt_response,
|
|
19
|
+
decrypt_responses,
|
|
20
|
+
encrypt_request,
|
|
21
|
+
)
|
|
22
|
+
from langgraph_api.feature_flags import (
|
|
23
|
+
IS_POSTGRES_OR_GRPC_BACKEND,
|
|
24
|
+
USE_RUNTIME_CONTEXT_API,
|
|
25
|
+
)
|
|
16
26
|
from langgraph_api.graph import get_assistant_id, get_graph
|
|
27
|
+
from langgraph_api.grpc.ops import Assistants as GrpcAssistants
|
|
17
28
|
from langgraph_api.js.base import BaseRemotePregel
|
|
18
29
|
from langgraph_api.route import ApiRequest, ApiResponse, ApiRoute
|
|
19
|
-
from langgraph_api.schema import ASSISTANT_FIELDS
|
|
20
|
-
from langgraph_api.serde import
|
|
30
|
+
from langgraph_api.schema import ASSISTANT_ENCRYPTION_FIELDS, ASSISTANT_FIELDS
|
|
31
|
+
from langgraph_api.serde import json_loads
|
|
21
32
|
from langgraph_api.utils import (
|
|
22
33
|
fetchone,
|
|
23
34
|
get_pagination_headers,
|
|
24
35
|
validate_select_columns,
|
|
25
36
|
validate_uuid,
|
|
26
37
|
)
|
|
38
|
+
from langgraph_api.utils.headers import get_configurable_headers
|
|
27
39
|
from langgraph_api.validation import (
|
|
28
40
|
AssistantCountRequest,
|
|
29
41
|
AssistantCreate,
|
|
@@ -31,14 +43,22 @@ from langgraph_api.validation import (
|
|
|
31
43
|
AssistantSearchRequest,
|
|
32
44
|
AssistantVersionChange,
|
|
33
45
|
AssistantVersionsSearchRequest,
|
|
46
|
+
ConfigValidator,
|
|
34
47
|
)
|
|
35
48
|
from langgraph_runtime.checkpoint import Checkpointer
|
|
36
|
-
from langgraph_runtime.database import connect
|
|
37
|
-
from langgraph_runtime.ops import Assistants
|
|
49
|
+
from langgraph_runtime.database import connect as base_connect
|
|
38
50
|
from langgraph_runtime.retry import retry_db
|
|
39
51
|
|
|
40
52
|
logger = structlog.stdlib.get_logger(__name__)
|
|
41
53
|
|
|
54
|
+
if IS_POSTGRES_OR_GRPC_BACKEND:
|
|
55
|
+
CrudAssistants = GrpcAssistants
|
|
56
|
+
else:
|
|
57
|
+
from langgraph_runtime.ops import Assistants
|
|
58
|
+
|
|
59
|
+
CrudAssistants = Assistants
|
|
60
|
+
|
|
61
|
+
connect = partial(base_connect, supports_core_api=IS_POSTGRES_OR_GRPC_BACKEND)
|
|
42
62
|
|
|
43
63
|
EXCLUDED_CONFIG_SCHEMA = (
|
|
44
64
|
"__pregel_checkpointer",
|
|
@@ -54,7 +74,7 @@ def _get_configurable_jsonschema(graph: Pregel) -> dict:
|
|
|
54
74
|
|
|
55
75
|
Important: we only return the `configurable` part of the schema.
|
|
56
76
|
|
|
57
|
-
The default get_config_schema method returns the entire schema (
|
|
77
|
+
The default get_config_schema method returns the entire schema (Config),
|
|
58
78
|
which includes other root keys like "max_concurrency", which we
|
|
59
79
|
do not want to expose.
|
|
60
80
|
|
|
@@ -110,21 +130,21 @@ def _graph_schemas(graph: Pregel) -> dict:
|
|
|
110
130
|
input_schema = graph.get_input_jsonschema()
|
|
111
131
|
except Exception as e:
|
|
112
132
|
logger.warning(
|
|
113
|
-
f"Failed to get input schema for graph {graph.name} with error: `{
|
|
133
|
+
f"Failed to get input schema for graph {graph.name} with error: `{e!s}`"
|
|
114
134
|
)
|
|
115
135
|
input_schema = None
|
|
116
136
|
try:
|
|
117
137
|
output_schema = graph.get_output_jsonschema()
|
|
118
138
|
except Exception as e:
|
|
119
139
|
logger.warning(
|
|
120
|
-
f"Failed to get output schema for graph {graph.name} with error: `{
|
|
140
|
+
f"Failed to get output schema for graph {graph.name} with error: `{e!s}`"
|
|
121
141
|
)
|
|
122
142
|
output_schema = None
|
|
123
143
|
try:
|
|
124
144
|
state_schema = _state_jsonschema(graph)
|
|
125
145
|
except Exception as e:
|
|
126
146
|
logger.warning(
|
|
127
|
-
f"Failed to get state schema for graph {graph.name} with error: `{
|
|
147
|
+
f"Failed to get state schema for graph {graph.name} with error: `{e!s}`"
|
|
128
148
|
)
|
|
129
149
|
state_schema = None
|
|
130
150
|
|
|
@@ -132,7 +152,7 @@ def _graph_schemas(graph: Pregel) -> dict:
|
|
|
132
152
|
config_schema = _get_configurable_jsonschema(graph)
|
|
133
153
|
except Exception as e:
|
|
134
154
|
logger.warning(
|
|
135
|
-
f"Failed to get config schema for graph {graph.name} with error: `{
|
|
155
|
+
f"Failed to get config schema for graph {graph.name} with error: `{e!s}`"
|
|
136
156
|
)
|
|
137
157
|
config_schema = None
|
|
138
158
|
|
|
@@ -141,7 +161,7 @@ def _graph_schemas(graph: Pregel) -> dict:
|
|
|
141
161
|
context_schema = graph.get_context_jsonschema()
|
|
142
162
|
except Exception as e:
|
|
143
163
|
logger.warning(
|
|
144
|
-
f"Failed to get context schema for graph {graph.name} with error: `{
|
|
164
|
+
f"Failed to get context schema for graph {graph.name} with error: `{e!s}`"
|
|
145
165
|
)
|
|
146
166
|
context_schema = graph.config_schema() # type: ignore[deprecated]
|
|
147
167
|
else:
|
|
@@ -162,19 +182,40 @@ async def create_assistant(request: ApiRequest) -> ApiResponse:
|
|
|
162
182
|
payload = await request.json(AssistantCreate)
|
|
163
183
|
if assistant_id := payload.get("assistant_id"):
|
|
164
184
|
validate_uuid(assistant_id, "Invalid assistant ID: must be a UUID")
|
|
185
|
+
config = payload.get("config")
|
|
186
|
+
if config:
|
|
187
|
+
try:
|
|
188
|
+
ConfigValidator.validate(config)
|
|
189
|
+
except jsonschema_rs.ValidationError as e:
|
|
190
|
+
raise HTTPException(status_code=422, detail=str(e)) from e
|
|
191
|
+
|
|
192
|
+
encrypted_payload = await encrypt_request(
|
|
193
|
+
payload,
|
|
194
|
+
"assistant",
|
|
195
|
+
ASSISTANT_ENCRYPTION_FIELDS,
|
|
196
|
+
)
|
|
197
|
+
|
|
165
198
|
async with connect() as conn:
|
|
166
|
-
assistant = await
|
|
199
|
+
assistant = await CrudAssistants.put(
|
|
167
200
|
conn,
|
|
168
201
|
assistant_id or str(uuid4()),
|
|
169
|
-
config=
|
|
170
|
-
context=
|
|
202
|
+
config=encrypted_payload.get("config") or {},
|
|
203
|
+
context=encrypted_payload.get("context"), # None if not provided
|
|
171
204
|
graph_id=payload["graph_id"],
|
|
172
|
-
metadata=
|
|
205
|
+
metadata=encrypted_payload.get("metadata") or {},
|
|
173
206
|
if_exists=payload.get("if_exists") or "raise",
|
|
174
207
|
name=payload.get("name") or "Untitled",
|
|
175
208
|
description=payload.get("description"),
|
|
176
209
|
)
|
|
177
|
-
|
|
210
|
+
|
|
211
|
+
# Decrypt metadata, config, and context in response
|
|
212
|
+
assistant_data = await fetchone(assistant, not_found_code=409)
|
|
213
|
+
assistant_data = await decrypt_response(
|
|
214
|
+
assistant_data,
|
|
215
|
+
"assistant",
|
|
216
|
+
ASSISTANT_ENCRYPTION_FIELDS,
|
|
217
|
+
)
|
|
218
|
+
return ApiResponse(assistant_data)
|
|
178
219
|
|
|
179
220
|
|
|
180
221
|
@retry_db
|
|
@@ -185,10 +226,17 @@ async def search_assistants(
|
|
|
185
226
|
payload = await request.json(AssistantSearchRequest)
|
|
186
227
|
select = validate_select_columns(payload.get("select") or None, ASSISTANT_FIELDS)
|
|
187
228
|
offset = int(payload.get("offset") or 0)
|
|
229
|
+
config = payload.get("config")
|
|
230
|
+
if config:
|
|
231
|
+
try:
|
|
232
|
+
ConfigValidator.validate(config)
|
|
233
|
+
except jsonschema_rs.ValidationError as e:
|
|
234
|
+
raise HTTPException(status_code=422, detail=str(e)) from e
|
|
188
235
|
async with connect() as conn:
|
|
189
|
-
assistants_iter, next_offset = await
|
|
236
|
+
assistants_iter, next_offset = await CrudAssistants.search(
|
|
190
237
|
conn,
|
|
191
238
|
graph_id=payload.get("graph_id"),
|
|
239
|
+
name=payload.get("name"),
|
|
192
240
|
metadata=payload.get("metadata"),
|
|
193
241
|
limit=int(payload.get("limit") or 10),
|
|
194
242
|
offset=offset,
|
|
@@ -199,7 +247,15 @@ async def search_assistants(
|
|
|
199
247
|
assistants, response_headers = await get_pagination_headers(
|
|
200
248
|
assistants_iter, next_offset, offset
|
|
201
249
|
)
|
|
202
|
-
|
|
250
|
+
|
|
251
|
+
# Decrypt metadata, config, and context in all returned assistants
|
|
252
|
+
decrypted_assistants = await decrypt_responses(
|
|
253
|
+
assistants,
|
|
254
|
+
"assistant",
|
|
255
|
+
ASSISTANT_ENCRYPTION_FIELDS,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return ApiResponse(decrypted_assistants, headers=response_headers)
|
|
203
259
|
|
|
204
260
|
|
|
205
261
|
@retry_db
|
|
@@ -209,9 +265,10 @@ async def count_assistants(
|
|
|
209
265
|
"""Count assistants."""
|
|
210
266
|
payload = await request.json(AssistantCountRequest)
|
|
211
267
|
async with connect() as conn:
|
|
212
|
-
count = await
|
|
268
|
+
count = await CrudAssistants.count(
|
|
213
269
|
conn,
|
|
214
270
|
graph_id=payload.get("graph_id"),
|
|
271
|
+
name=payload.get("name"),
|
|
215
272
|
metadata=payload.get("metadata"),
|
|
216
273
|
)
|
|
217
274
|
return ApiResponse(count)
|
|
@@ -225,8 +282,16 @@ async def get_assistant(
|
|
|
225
282
|
"""Get an assistant by ID."""
|
|
226
283
|
validate_uuid(assistant_id, "Invalid assistant ID: must be a UUID")
|
|
227
284
|
async with connect() as conn:
|
|
228
|
-
assistant = await
|
|
229
|
-
|
|
285
|
+
assistant = await CrudAssistants.get(conn, assistant_id)
|
|
286
|
+
|
|
287
|
+
# Decrypt metadata, config, and context in response
|
|
288
|
+
assistant_data = await fetchone(assistant)
|
|
289
|
+
assistant_data = await decrypt_response(
|
|
290
|
+
assistant_data,
|
|
291
|
+
"assistant",
|
|
292
|
+
ASSISTANT_ENCRYPTION_FIELDS,
|
|
293
|
+
)
|
|
294
|
+
return ApiResponse(assistant_data)
|
|
230
295
|
|
|
231
296
|
|
|
232
297
|
@retry_db
|
|
@@ -237,14 +302,18 @@ async def get_assistant_graph(
|
|
|
237
302
|
assistant_id = get_assistant_id(str(request.path_params["assistant_id"]))
|
|
238
303
|
validate_uuid(assistant_id, "Invalid assistant ID: must be a UUID")
|
|
239
304
|
async with connect() as conn:
|
|
240
|
-
assistant_ = await
|
|
305
|
+
assistant_ = await CrudAssistants.get(conn, assistant_id)
|
|
241
306
|
assistant = await fetchone(assistant_)
|
|
242
|
-
config =
|
|
307
|
+
config = json_loads(assistant["config"])
|
|
308
|
+
configurable = config.setdefault("configurable", {})
|
|
309
|
+
configurable.update(get_configurable_headers(request.headers))
|
|
310
|
+
|
|
243
311
|
async with get_graph(
|
|
244
312
|
assistant["graph_id"],
|
|
245
313
|
config,
|
|
246
314
|
checkpointer=Checkpointer(),
|
|
247
315
|
store=(await api_store.get_store()),
|
|
316
|
+
is_for_execution=False,
|
|
248
317
|
) as graph:
|
|
249
318
|
xray: bool | int = False
|
|
250
319
|
xray_query = request.query_params.get("xray")
|
|
@@ -291,41 +360,45 @@ async def get_assistant_subgraphs(
|
|
|
291
360
|
assistant_id = request.path_params["assistant_id"]
|
|
292
361
|
validate_uuid(assistant_id, "Invalid assistant ID: must be a UUID")
|
|
293
362
|
async with connect() as conn:
|
|
294
|
-
assistant_ = await
|
|
363
|
+
assistant_ = await CrudAssistants.get(conn, assistant_id)
|
|
295
364
|
assistant = await fetchone(assistant_)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
365
|
+
|
|
366
|
+
config = json_loads(assistant["config"])
|
|
367
|
+
configurable = config.setdefault("configurable", {})
|
|
368
|
+
configurable.update(get_configurable_headers(request.headers))
|
|
369
|
+
async with get_graph(
|
|
370
|
+
assistant["graph_id"],
|
|
371
|
+
config,
|
|
372
|
+
checkpointer=Checkpointer(),
|
|
373
|
+
store=(await api_store.get_store()),
|
|
374
|
+
is_for_execution=False,
|
|
375
|
+
) as graph:
|
|
376
|
+
namespace = request.path_params.get("namespace")
|
|
377
|
+
|
|
378
|
+
if isinstance(graph, BaseRemotePregel):
|
|
379
|
+
return ApiResponse(
|
|
380
|
+
await graph.fetch_subgraphs(
|
|
381
|
+
namespace=namespace,
|
|
382
|
+
recurse=request.query_params.get("recurse", "False")
|
|
383
|
+
in ("true", "True"),
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
return ApiResponse(
|
|
389
|
+
{
|
|
390
|
+
ns: _graph_schemas(subgraph)
|
|
391
|
+
async for ns, subgraph in graph.aget_subgraphs(
|
|
308
392
|
namespace=namespace,
|
|
309
393
|
recurse=request.query_params.get("recurse", "False")
|
|
310
394
|
in ("true", "True"),
|
|
311
395
|
)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
async for ns, subgraph in graph.aget_subgraphs(
|
|
319
|
-
namespace=namespace,
|
|
320
|
-
recurse=request.query_params.get("recurse", "False")
|
|
321
|
-
in ("true", "True"),
|
|
322
|
-
)
|
|
323
|
-
}
|
|
324
|
-
)
|
|
325
|
-
except NotImplementedError:
|
|
326
|
-
raise HTTPException(
|
|
327
|
-
422, detail="The graph does not support visualization"
|
|
328
|
-
) from None
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
except NotImplementedError:
|
|
399
|
+
raise HTTPException(
|
|
400
|
+
422, detail="The graph does not support visualization"
|
|
401
|
+
) from None
|
|
329
402
|
|
|
330
403
|
|
|
331
404
|
@retry_db
|
|
@@ -336,38 +409,41 @@ async def get_assistant_schemas(
|
|
|
336
409
|
assistant_id = request.path_params["assistant_id"]
|
|
337
410
|
validate_uuid(assistant_id, "Invalid assistant ID: must be a UUID")
|
|
338
411
|
async with connect() as conn:
|
|
339
|
-
assistant_ = await
|
|
340
|
-
# TODO Implementa cache so we can de-dent and release this connection.
|
|
412
|
+
assistant_ = await CrudAssistants.get(conn, assistant_id)
|
|
341
413
|
assistant = await fetchone(assistant_)
|
|
342
|
-
config = await ajson_loads(assistant["config"])
|
|
343
|
-
async with get_graph(
|
|
344
|
-
assistant["graph_id"],
|
|
345
|
-
config,
|
|
346
|
-
checkpointer=Checkpointer(),
|
|
347
|
-
store=(await api_store.get_store()),
|
|
348
|
-
) as graph:
|
|
349
|
-
if isinstance(graph, BaseRemotePregel):
|
|
350
|
-
schemas = await graph.fetch_state_schema()
|
|
351
|
-
return ApiResponse(
|
|
352
|
-
{
|
|
353
|
-
"graph_id": assistant["graph_id"],
|
|
354
|
-
"input_schema": schemas.get("input"),
|
|
355
|
-
"output_schema": schemas.get("output"),
|
|
356
|
-
"state_schema": schemas.get("state"),
|
|
357
|
-
"config_schema": schemas.get("config"),
|
|
358
|
-
"context_schema": schemas.get("context"),
|
|
359
|
-
}
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
schemas = _graph_schemas(graph)
|
|
363
414
|
|
|
415
|
+
config = json_loads(assistant["config"])
|
|
416
|
+
configurable = config.setdefault("configurable", {})
|
|
417
|
+
configurable.update(get_configurable_headers(request.headers))
|
|
418
|
+
async with get_graph(
|
|
419
|
+
assistant["graph_id"],
|
|
420
|
+
config,
|
|
421
|
+
checkpointer=Checkpointer(),
|
|
422
|
+
store=(await api_store.get_store()),
|
|
423
|
+
is_for_execution=False,
|
|
424
|
+
) as graph:
|
|
425
|
+
if isinstance(graph, BaseRemotePregel):
|
|
426
|
+
schemas = await graph.fetch_state_schema()
|
|
364
427
|
return ApiResponse(
|
|
365
428
|
{
|
|
366
429
|
"graph_id": assistant["graph_id"],
|
|
367
|
-
|
|
430
|
+
"input_schema": schemas.get("input"),
|
|
431
|
+
"output_schema": schemas.get("output"),
|
|
432
|
+
"state_schema": schemas.get("state"),
|
|
433
|
+
"config_schema": schemas.get("config"),
|
|
434
|
+
"context_schema": schemas.get("context"),
|
|
368
435
|
}
|
|
369
436
|
)
|
|
370
437
|
|
|
438
|
+
schemas = _graph_schemas(graph)
|
|
439
|
+
|
|
440
|
+
return ApiResponse(
|
|
441
|
+
{
|
|
442
|
+
"graph_id": assistant["graph_id"],
|
|
443
|
+
**schemas,
|
|
444
|
+
}
|
|
445
|
+
)
|
|
446
|
+
|
|
371
447
|
|
|
372
448
|
@retry_db
|
|
373
449
|
async def patch_assistant(
|
|
@@ -377,27 +453,58 @@ async def patch_assistant(
|
|
|
377
453
|
assistant_id = request.path_params["assistant_id"]
|
|
378
454
|
validate_uuid(assistant_id, "Invalid assistant ID: must be a UUID")
|
|
379
455
|
payload = await request.json(AssistantPatch)
|
|
456
|
+
config = payload.get("config")
|
|
457
|
+
if config:
|
|
458
|
+
try:
|
|
459
|
+
ConfigValidator.validate(config)
|
|
460
|
+
except jsonschema_rs.ValidationError as e:
|
|
461
|
+
raise HTTPException(status_code=422, detail=str(e)) from e
|
|
462
|
+
|
|
463
|
+
encrypted_fields = await encrypt_request(
|
|
464
|
+
payload,
|
|
465
|
+
"assistant",
|
|
466
|
+
ASSISTANT_ENCRYPTION_FIELDS,
|
|
467
|
+
)
|
|
468
|
+
|
|
380
469
|
async with connect() as conn:
|
|
381
|
-
assistant = await
|
|
470
|
+
assistant = await CrudAssistants.patch(
|
|
382
471
|
conn,
|
|
383
472
|
assistant_id,
|
|
384
|
-
config=
|
|
385
|
-
context=
|
|
473
|
+
config=encrypted_fields.get("config"),
|
|
474
|
+
context=encrypted_fields.get("context"),
|
|
386
475
|
graph_id=payload.get("graph_id"),
|
|
387
|
-
metadata=
|
|
476
|
+
metadata=encrypted_fields.get("metadata"),
|
|
388
477
|
name=payload.get("name"),
|
|
389
478
|
description=payload.get("description"),
|
|
390
479
|
)
|
|
391
|
-
|
|
480
|
+
|
|
481
|
+
# Decrypt metadata, config, and context in response
|
|
482
|
+
assistant_data = await fetchone(assistant)
|
|
483
|
+
assistant_data = await decrypt_response(
|
|
484
|
+
assistant_data,
|
|
485
|
+
"assistant",
|
|
486
|
+
ASSISTANT_ENCRYPTION_FIELDS,
|
|
487
|
+
)
|
|
488
|
+
return ApiResponse(assistant_data)
|
|
392
489
|
|
|
393
490
|
|
|
394
491
|
@retry_db
|
|
395
492
|
async def delete_assistant(request: ApiRequest) -> Response:
|
|
396
|
-
"""Delete an assistant by ID.
|
|
493
|
+
"""Delete an assistant by ID.
|
|
494
|
+
|
|
495
|
+
Query params:
|
|
496
|
+
delete_threads: If "true", delete all threads where
|
|
497
|
+
metadata.assistant_id matches this assistant.
|
|
498
|
+
"""
|
|
397
499
|
assistant_id = request.path_params["assistant_id"]
|
|
398
500
|
validate_uuid(assistant_id, "Invalid assistant ID: must be a UUID")
|
|
399
|
-
|
|
400
|
-
|
|
501
|
+
delete_threads = request.query_params.get("delete_threads", "").lower() == "true"
|
|
502
|
+
|
|
503
|
+
aid = await CrudAssistants.delete(
|
|
504
|
+
None,
|
|
505
|
+
assistant_id,
|
|
506
|
+
delete_threads=delete_threads,
|
|
507
|
+
)
|
|
401
508
|
await fetchone(aid)
|
|
402
509
|
return Response(status_code=204)
|
|
403
510
|
|
|
@@ -409,7 +516,7 @@ async def get_assistant_versions(request: ApiRequest) -> ApiResponse:
|
|
|
409
516
|
validate_uuid(assistant_id, "Invalid assistant ID: must be a UUID")
|
|
410
517
|
payload = await request.json(AssistantVersionsSearchRequest)
|
|
411
518
|
async with connect() as conn:
|
|
412
|
-
assistants_iter = await
|
|
519
|
+
assistants_iter = await CrudAssistants.get_versions(
|
|
413
520
|
conn,
|
|
414
521
|
assistant_id,
|
|
415
522
|
metadata=payload.get("metadata") or {},
|
|
@@ -421,7 +528,15 @@ async def get_assistant_versions(request: ApiRequest) -> ApiResponse:
|
|
|
421
528
|
raise HTTPException(
|
|
422
529
|
status_code=404, detail=f"Assistant {assistant_id} not found"
|
|
423
530
|
)
|
|
424
|
-
|
|
531
|
+
|
|
532
|
+
# Decrypt metadata, config, and context in all assistant versions
|
|
533
|
+
decrypted_assistants = await decrypt_responses(
|
|
534
|
+
assistants,
|
|
535
|
+
"assistant",
|
|
536
|
+
ASSISTANT_ENCRYPTION_FIELDS,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
return ApiResponse(decrypted_assistants)
|
|
425
540
|
|
|
426
541
|
|
|
427
542
|
@retry_db
|
|
@@ -431,10 +546,18 @@ async def set_latest_assistant_version(request: ApiRequest) -> ApiResponse:
|
|
|
431
546
|
payload = await request.json(AssistantVersionChange)
|
|
432
547
|
validate_uuid(assistant_id, "Invalid assistant ID: must be a UUID")
|
|
433
548
|
async with connect() as conn:
|
|
434
|
-
assistant = await
|
|
549
|
+
assistant = await CrudAssistants.set_latest(
|
|
435
550
|
conn, assistant_id, payload.get("version")
|
|
436
551
|
)
|
|
437
|
-
|
|
552
|
+
|
|
553
|
+
# Decrypt metadata, config, and context in response
|
|
554
|
+
assistant_data = await fetchone(assistant, not_found_code=404)
|
|
555
|
+
assistant_data = await decrypt_response(
|
|
556
|
+
assistant_data,
|
|
557
|
+
"assistant",
|
|
558
|
+
ASSISTANT_ENCRYPTION_FIELDS,
|
|
559
|
+
)
|
|
560
|
+
return ApiResponse(assistant_data)
|
|
438
561
|
|
|
439
562
|
|
|
440
563
|
assistants_routes: list[BaseRoute] = [
|
langgraph_api/api/mcp.py
CHANGED
|
@@ -193,13 +193,13 @@ async def handle_post_request(request: ApiRequest) -> Response:
|
|
|
193
193
|
# Careful ID checks as the integer 0 is a valid ID
|
|
194
194
|
if id_ is not None and method:
|
|
195
195
|
# JSON-RPC request
|
|
196
|
-
return await handle_jsonrpc_request(request, cast(JsonRpcRequest, message))
|
|
196
|
+
return await handle_jsonrpc_request(request, cast("JsonRpcRequest", message))
|
|
197
197
|
elif id_ is not None:
|
|
198
198
|
# JSON-RPC response
|
|
199
|
-
return handle_jsonrpc_response(cast(JsonRpcResponse, message))
|
|
199
|
+
return handle_jsonrpc_response(cast("JsonRpcResponse", message))
|
|
200
200
|
elif method:
|
|
201
201
|
# JSON-RPC notification
|
|
202
|
-
return handle_jsonrpc_notification(cast(JsonRpcNotification, message))
|
|
202
|
+
return handle_jsonrpc_notification(cast("JsonRpcNotification", message))
|
|
203
203
|
else:
|
|
204
204
|
# Invalid message format
|
|
205
205
|
return create_error_response(
|
langgraph_api/api/meta.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
from typing import cast
|
|
2
|
-
|
|
3
1
|
import langgraph.version
|
|
2
|
+
import structlog
|
|
4
3
|
from starlette.responses import JSONResponse, PlainTextResponse
|
|
5
4
|
|
|
6
5
|
from langgraph_api import __version__, config, metadata
|
|
6
|
+
from langgraph_api.feature_flags import FF_USE_CORE_API
|
|
7
|
+
from langgraph_api.grpc.ops import Runs as GrpcRuns
|
|
7
8
|
from langgraph_api.http_metrics import HTTP_METRICS_COLLECTOR
|
|
8
9
|
from langgraph_api.route import ApiRequest
|
|
9
10
|
from langgraph_license.validation import plus_features_enabled
|
|
@@ -11,8 +12,12 @@ from langgraph_runtime.database import connect, pool_stats
|
|
|
11
12
|
from langgraph_runtime.metrics import get_metrics
|
|
12
13
|
from langgraph_runtime.ops import Runs
|
|
13
14
|
|
|
15
|
+
CrudRuns = GrpcRuns if FF_USE_CORE_API else Runs
|
|
16
|
+
|
|
14
17
|
METRICS_FORMATS = {"prometheus", "json"}
|
|
15
18
|
|
|
19
|
+
logger = structlog.stdlib.get_logger(__name__)
|
|
20
|
+
|
|
16
21
|
|
|
17
22
|
async def meta_info(request: ApiRequest):
|
|
18
23
|
plus = plus_features_enabled()
|
|
@@ -23,7 +28,8 @@ async def meta_info(request: ApiRequest):
|
|
|
23
28
|
"flags": {
|
|
24
29
|
"assistants": True,
|
|
25
30
|
"crons": plus and config.FF_CRONS_ENABLED,
|
|
26
|
-
"langsmith": bool(config.
|
|
31
|
+
"langsmith": bool(config.LANGSMITH_CONTROL_PLANE_API_KEY)
|
|
32
|
+
and bool(config.TRACING),
|
|
27
33
|
"langsmith_tracing_replicas": True,
|
|
28
34
|
},
|
|
29
35
|
"host": {
|
|
@@ -45,7 +51,7 @@ async def meta_metrics(request: ApiRequest):
|
|
|
45
51
|
|
|
46
52
|
# collect stats
|
|
47
53
|
metrics = get_metrics()
|
|
48
|
-
worker_metrics =
|
|
54
|
+
worker_metrics = metrics["workers"]
|
|
49
55
|
workers_max = worker_metrics["max"]
|
|
50
56
|
workers_active = worker_metrics["active"]
|
|
51
57
|
workers_available = worker_metrics["available"]
|
|
@@ -64,42 +70,60 @@ async def meta_metrics(request: ApiRequest):
|
|
|
64
70
|
async with connect() as conn:
|
|
65
71
|
resp = {
|
|
66
72
|
**pg_redis_stats,
|
|
67
|
-
"queue": await
|
|
73
|
+
"queue": await CrudRuns.stats(conn),
|
|
68
74
|
**http_metrics,
|
|
69
75
|
}
|
|
70
76
|
if config.N_JOBS_PER_WORKER > 0:
|
|
71
77
|
resp["workers"] = worker_metrics
|
|
72
78
|
return JSONResponse(resp)
|
|
73
79
|
elif metrics_format == "prometheus":
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
metrics = []
|
|
81
|
+
try:
|
|
82
|
+
async with connect() as conn:
|
|
83
|
+
queue_stats = await CrudRuns.stats(conn)
|
|
76
84
|
|
|
77
|
-
metrics = [
|
|
78
|
-
"# HELP lg_api_num_pending_runs The number of runs currently pending.",
|
|
79
|
-
"# TYPE lg_api_num_pending_runs gauge",
|
|
80
|
-
f'lg_api_num_pending_runs{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {queue_stats["n_pending"]}',
|
|
81
|
-
"# HELP lg_api_num_running_runs The number of runs currently running.",
|
|
82
|
-
"# TYPE lg_api_num_running_runs gauge",
|
|
83
|
-
f'lg_api_num_running_runs{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {queue_stats["n_running"]}',
|
|
84
|
-
]
|
|
85
|
-
|
|
86
|
-
if config.N_JOBS_PER_WORKER > 0:
|
|
87
85
|
metrics.extend(
|
|
88
86
|
[
|
|
89
|
-
"# HELP
|
|
90
|
-
"# TYPE
|
|
91
|
-
f'
|
|
92
|
-
"# HELP
|
|
93
|
-
"# TYPE
|
|
94
|
-
f'
|
|
95
|
-
"# HELP
|
|
96
|
-
"# TYPE
|
|
97
|
-
f'
|
|
87
|
+
"# HELP lg_api_num_pending_runs The number of runs currently pending.",
|
|
88
|
+
"# TYPE lg_api_num_pending_runs gauge",
|
|
89
|
+
f'lg_api_num_pending_runs{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {queue_stats["n_pending"]}',
|
|
90
|
+
"# HELP lg_api_num_running_runs The number of runs currently running.",
|
|
91
|
+
"# TYPE lg_api_num_running_runs gauge",
|
|
92
|
+
f'lg_api_num_running_runs{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {queue_stats["n_running"]}',
|
|
93
|
+
"# HELP lg_api_pending_runs_wait_time_max The maximum time a run has been pending, in seconds.",
|
|
94
|
+
"# TYPE lg_api_pending_runs_wait_time_max gauge",
|
|
95
|
+
f'lg_api_pending_runs_wait_time_max{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {queue_stats.get("pending_runs_wait_time_max_secs") or 0}',
|
|
96
|
+
"# HELP lg_api_pending_runs_wait_time_med The median pending wait time across runs, in seconds.",
|
|
97
|
+
"# TYPE lg_api_pending_runs_wait_time_med gauge",
|
|
98
|
+
f'lg_api_pending_runs_wait_time_med{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {queue_stats.get("pending_runs_wait_time_med_secs") or 0}',
|
|
99
|
+
"# HELP lg_api_pending_unblocked_runs_wait_time_max The maximum time a run has been pending excluding runs blocked by another run on the same thread, in seconds.",
|
|
100
|
+
"# TYPE lg_api_pending_unblocked_runs_wait_time_max gauge",
|
|
101
|
+
f'lg_api_pending_unblocked_runs_wait_time_max{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {queue_stats.get("pending_unblocked_runs_wait_time_max_secs") or 0}',
|
|
98
102
|
]
|
|
99
103
|
)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
# if we get a db connection error/timeout, just skip queue stats
|
|
106
|
+
await logger.awarning(
|
|
107
|
+
"Ignoring error while getting run stats for /metrics", exc_info=e
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if config.N_JOBS_PER_WORKER > 0:
|
|
111
|
+
metrics.extend(
|
|
112
|
+
[
|
|
113
|
+
"# HELP lg_api_workers_max The maximum number of workers available.",
|
|
114
|
+
"# TYPE lg_api_workers_max gauge",
|
|
115
|
+
f'lg_api_workers_max{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {workers_max}',
|
|
116
|
+
"# HELP lg_api_workers_active The number of currently active workers.",
|
|
117
|
+
"# TYPE lg_api_workers_active gauge",
|
|
118
|
+
f'lg_api_workers_active{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {workers_active}',
|
|
119
|
+
"# HELP lg_api_workers_available The number of available (idle) workers.",
|
|
120
|
+
"# TYPE lg_api_workers_available gauge",
|
|
121
|
+
f'lg_api_workers_available{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {workers_available}',
|
|
122
|
+
]
|
|
123
|
+
)
|
|
100
124
|
|
|
101
|
-
|
|
102
|
-
|
|
125
|
+
metrics.extend(http_metrics)
|
|
126
|
+
metrics.extend(pg_redis_stats)
|
|
103
127
|
|
|
104
128
|
metrics_response = "\n".join(metrics)
|
|
105
129
|
return PlainTextResponse(metrics_response)
|