langgraph-api 0.18.0.dev2__py3-none-any.whl → 0.18.0.dev4__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/a2a.py +93 -10
- langgraph_api/api/event_streaming.py +81 -9
- langgraph_api/api/mcp/_calls.py +29 -0
- langgraph_api/api/mcp/_constants.py +15 -6
- langgraph_api/api/mcp/_handlers.py +146 -196
- langgraph_api/api/mcp/_outcome.py +43 -0
- langgraph_api/api/mcp/_protocol.py +224 -0
- langgraph_api/config/__init__.py +38 -4
- langgraph_api/cron_scheduler.py +2 -2
- langgraph_api/drain.py +164 -0
- langgraph_api/event_streaming/service.py +130 -133
- langgraph_api/event_streaming/session.py +11 -6
- langgraph_api/executor/__init__.py +9 -0
- langgraph_api/executor/__main__.py +343 -0
- langgraph_api/feature_flags.py +6 -0
- langgraph_api/graph.py +12 -2
- langgraph_api/grpc/ops/runs.py +41 -5
- langgraph_api/grpc/ops/threads.py +54 -6
- langgraph_api/metrics_otlp.py +8 -0
- langgraph_api/models/run.py +11 -3
- langgraph_api/queue_entrypoint.py +13 -1
- langgraph_api/self_hosted_logs.py +3 -1
- langgraph_api/self_hosted_metrics.py +1 -1
- langgraph_api/stream.py +49 -3
- langgraph_api/timing/__init__.py +16 -0
- langgraph_api/timing/startup.py +184 -0
- langgraph_api/timing/timer.py +16 -2
- langgraph_api/utils/headers.py +20 -3
- langgraph_api/validation.py +11 -5
- langgraph_api/worker.py +146 -13
- langgraph_api/worker_runtime.py +124 -4
- {langgraph_api-0.18.0.dev2.dist-info → langgraph_api-0.18.0.dev4.dist-info}/METADATA +1 -1
- {langgraph_api-0.18.0.dev2.dist-info → langgraph_api-0.18.0.dev4.dist-info}/RECORD +43 -38
- langgraph_grpc_common/proto/__init__.py +4 -4
- langgraph_grpc_common/proto/orchestrator_pb2.py +50 -0
- langgraph_grpc_common/proto/{runner_pb2.pyi → orchestrator_pb2.pyi} +4 -4
- langgraph_grpc_common/proto/orchestrator_pb2_grpc.py +108 -0
- langgraph_grpc_common/proto/orchestrator_pb2_grpc.pyi +82 -0
- openapi.json +20 -0
- langgraph_api/runner/__init__.py +0 -10
- langgraph_api/runner/__main__.py +0 -275
- langgraph_grpc_common/proto/runner_pb2.py +0 -50
- langgraph_grpc_common/proto/runner_pb2_grpc.py +0 -108
- langgraph_grpc_common/proto/runner_pb2_grpc.pyi +0 -73
- {langgraph_api-0.18.0.dev2.dist-info → langgraph_api-0.18.0.dev4.dist-info}/WHEEL +0 -0
- {langgraph_api-0.18.0.dev2.dist-info → langgraph_api-0.18.0.dev4.dist-info}/entry_points.txt +0 -0
- {langgraph_api-0.18.0.dev2.dist-info → langgraph_api-0.18.0.dev4.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,29 +3,48 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import functools
|
|
5
5
|
import json
|
|
6
|
-
|
|
6
|
+
import re
|
|
7
|
+
from http import HTTPStatus
|
|
8
|
+
from typing import TYPE_CHECKING, Any, assert_never, cast
|
|
7
9
|
|
|
8
10
|
import structlog
|
|
9
11
|
from langgraph_sdk.client import LangGraphClient, get_client
|
|
10
12
|
from starlette.responses import JSONResponse, Response
|
|
11
13
|
|
|
12
|
-
from langgraph_api import
|
|
13
|
-
from langgraph_api.api.mcp import
|
|
14
|
+
from langgraph_api.api.mcp import _protocol, _sanitizers
|
|
15
|
+
from langgraph_api.api.mcp._calls import ToolCall
|
|
14
16
|
from langgraph_api.api.mcp._constants import (
|
|
15
17
|
DEFAULT_PAGE_SIZE,
|
|
16
|
-
|
|
18
|
+
ERROR_CODE_INVALID_REQUEST,
|
|
17
19
|
ERROR_CODE_METHOD_NOT_FOUND,
|
|
18
|
-
|
|
20
|
+
INVALID_CURSOR_MESSAGE,
|
|
21
|
+
LATEST_LEGACY_PROTOCOL_VERSION,
|
|
22
|
+
LEGACY_PROTOCOL_VERSIONS,
|
|
19
23
|
MAX_ASSISTANTS,
|
|
20
|
-
|
|
24
|
+
MAX_CURSOR_LENGTH,
|
|
25
|
+
MAX_PAGE_OFFSET,
|
|
26
|
+
PAUSED_TOOL_MESSAGE,
|
|
21
27
|
)
|
|
28
|
+
from langgraph_api.api.mcp._outcome import (
|
|
29
|
+
Completed,
|
|
30
|
+
Failed,
|
|
31
|
+
Paused,
|
|
32
|
+
RunOutcome,
|
|
33
|
+
parse_outcome,
|
|
34
|
+
)
|
|
35
|
+
from langgraph_api.serde import json_dumpb
|
|
22
36
|
|
|
23
37
|
if TYPE_CHECKING:
|
|
38
|
+
from collections.abc import Mapping
|
|
39
|
+
|
|
24
40
|
from langgraph_api.api.mcp._models import JsonRpcRequest
|
|
41
|
+
from langgraph_api.cache import JsonValue
|
|
25
42
|
from langgraph_api.route import ApiRequest
|
|
26
43
|
|
|
27
44
|
logger = structlog.stdlib.get_logger(__name__)
|
|
28
45
|
|
|
46
|
+
_CURSOR_DIGITS = re.compile(rf"[0-9]{{1,{MAX_CURSOR_LENGTH}}}")
|
|
47
|
+
|
|
29
48
|
|
|
30
49
|
@functools.lru_cache(maxsize=1)
|
|
31
50
|
def _client() -> LangGraphClient:
|
|
@@ -53,54 +72,43 @@ def handle_get_request() -> Response:
|
|
|
53
72
|
|
|
54
73
|
|
|
55
74
|
async def handle_post_request(request: ApiRequest) -> Response:
|
|
56
|
-
"""Handle HTTP POST requests for JSON-RPC messaging.
|
|
57
|
-
|
|
58
|
-
Args:
|
|
59
|
-
request: The incoming request object
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
Response to the JSON-RPC message
|
|
63
|
-
"""
|
|
64
75
|
body = await request.body()
|
|
65
76
|
|
|
66
|
-
# Validate JSON
|
|
67
77
|
try:
|
|
68
78
|
message = json.loads(body)
|
|
69
79
|
except json.JSONDecodeError:
|
|
70
80
|
return create_error_response("Invalid JSON", 400)
|
|
71
81
|
|
|
72
|
-
# Validate Accept header
|
|
73
82
|
if not is_valid_accept_header(request):
|
|
74
83
|
return create_error_response(
|
|
75
84
|
"Accept header must include application/json or text/event-stream", 400
|
|
76
85
|
)
|
|
77
86
|
|
|
78
|
-
# Validate message format
|
|
79
87
|
if not isinstance(message, dict):
|
|
80
88
|
return create_error_response("Invalid message format.", 400)
|
|
81
89
|
|
|
82
|
-
# Determine message type and route to appropriate handler
|
|
83
90
|
id_ = message.get("id")
|
|
84
91
|
method = message.get("method")
|
|
92
|
+
if (method is not None and not isinstance(method, str)) or (
|
|
93
|
+
id_ is not None and (isinstance(id_, bool) or not isinstance(id_, (str, int)))
|
|
94
|
+
):
|
|
95
|
+
return _jsonrpc_error(
|
|
96
|
+
None,
|
|
97
|
+
_protocol.ProtocolError(
|
|
98
|
+
"Invalid JSON-RPC method or id", ERROR_CODE_INVALID_REQUEST
|
|
99
|
+
),
|
|
100
|
+
)
|
|
85
101
|
|
|
86
|
-
# Check for required jsonrpc field
|
|
87
102
|
if message.get("jsonrpc") != "2.0":
|
|
88
103
|
return create_error_response(
|
|
89
104
|
"Invalid JSON-RPC message. Missing or invalid jsonrpc version.", 400
|
|
90
105
|
)
|
|
91
106
|
|
|
92
|
-
# Careful ID checks as the integer 0 is a valid ID
|
|
93
107
|
if id_ is not None and method:
|
|
94
|
-
# JSON-RPC request
|
|
95
108
|
return await handle_jsonrpc_request(request, cast("JsonRpcRequest", message))
|
|
96
|
-
elif id_ is not None:
|
|
97
|
-
# JSON-RPC response
|
|
98
|
-
return Response(status_code=202)
|
|
99
|
-
elif method:
|
|
100
|
-
# JSON-RPC notification
|
|
109
|
+
elif id_ is not None or method:
|
|
101
110
|
return Response(status_code=202)
|
|
102
111
|
else:
|
|
103
|
-
# Invalid message format
|
|
104
112
|
return create_error_response(
|
|
105
113
|
"Invalid message format. A message is to be either a JSON-RPC "
|
|
106
114
|
"request, response, or notification."
|
|
@@ -146,114 +154,85 @@ async def handle_jsonrpc_request(
|
|
|
146
154
|
request: ApiRequest,
|
|
147
155
|
message: JsonRpcRequest,
|
|
148
156
|
) -> Response:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
method = message["method"]
|
|
159
|
-
params = message.get("params", {})
|
|
160
|
-
|
|
161
|
-
if method == "initialize":
|
|
162
|
-
result_or_error = handle_initialize_request(message)
|
|
163
|
-
elif method == "ping":
|
|
164
|
-
result_or_error = {"result": {}}
|
|
165
|
-
elif method == "tools/list":
|
|
166
|
-
result_or_error = await handle_tools_list(request, params)
|
|
167
|
-
elif method == "tools/call":
|
|
168
|
-
result_or_error = await handle_tools_call(request, params)
|
|
169
|
-
else:
|
|
170
|
-
result_or_error = {
|
|
171
|
-
"error": {
|
|
172
|
-
"code": ERROR_CODE_METHOD_NOT_FOUND,
|
|
173
|
-
"message": f"Method not found: {method}",
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
# Process the result or error output
|
|
178
|
-
exists = {"error", "result"} - set(result_or_error.keys())
|
|
179
|
-
if len(exists) != 1:
|
|
180
|
-
raise AssertionError(
|
|
181
|
-
"Internal server error. Invalid response in MCP protocol implementation."
|
|
157
|
+
try:
|
|
158
|
+
call = _protocol.parse_call(request, message)
|
|
159
|
+
except _protocol.ProtocolError as error:
|
|
160
|
+
return _jsonrpc_error(message["id"], error)
|
|
161
|
+
try:
|
|
162
|
+
result = await _dispatch(request, call)
|
|
163
|
+
except _protocol.ProtocolError as error:
|
|
164
|
+
return _jsonrpc_error(
|
|
165
|
+
call.id, error, status=call.era.http_status(error), headers=call.era.headers
|
|
182
166
|
)
|
|
167
|
+
return _result_response(call, result)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def _dispatch(request: ApiRequest, call: _protocol.McpCall) -> dict[str, Any]:
|
|
171
|
+
match call.era, call.method:
|
|
172
|
+
case _protocol.Modern(), "server/discover":
|
|
173
|
+
return _protocol.discover()
|
|
174
|
+
case _protocol.Legacy(), "initialize":
|
|
175
|
+
return handle_initialize_request(call.message)
|
|
176
|
+
case _protocol.Legacy(), "ping":
|
|
177
|
+
return {}
|
|
178
|
+
case _, "tools/list":
|
|
179
|
+
return await handle_tools_list(request, call.params)
|
|
180
|
+
case _, "tools/call":
|
|
181
|
+
return await handle_tools_call(request, call)
|
|
182
|
+
case _:
|
|
183
|
+
raise _protocol.ProtocolError(
|
|
184
|
+
f"Method not found: {call.method}", ERROR_CODE_METHOD_NOT_FOUND
|
|
185
|
+
)
|
|
183
186
|
|
|
187
|
+
|
|
188
|
+
def _result_response(call: _protocol.McpCall, result: dict[str, Any]) -> Response:
|
|
184
189
|
return JSONResponse(
|
|
185
190
|
{
|
|
186
191
|
"jsonrpc": "2.0",
|
|
187
|
-
"id":
|
|
188
|
-
|
|
189
|
-
}
|
|
192
|
+
"id": call.id,
|
|
193
|
+
"result": call.era.result(call.method, result),
|
|
194
|
+
},
|
|
195
|
+
headers=call.era.headers,
|
|
190
196
|
)
|
|
191
197
|
|
|
192
198
|
|
|
193
|
-
def
|
|
194
|
-
|
|
199
|
+
def _jsonrpc_error(
|
|
200
|
+
id_: str | int | None,
|
|
201
|
+
error: _protocol.ProtocolError,
|
|
202
|
+
*,
|
|
203
|
+
status: HTTPStatus = HTTPStatus.BAD_REQUEST,
|
|
204
|
+
headers: Mapping[str, str] | None = None,
|
|
205
|
+
) -> Response:
|
|
206
|
+
return JSONResponse(
|
|
207
|
+
{"jsonrpc": "2.0", "id": id_, "error": error.error},
|
|
208
|
+
status_code=status,
|
|
209
|
+
headers=headers,
|
|
210
|
+
)
|
|
195
211
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if requested in
|
|
212
|
+
|
|
213
|
+
def _negotiate_protocol_version(requested: str) -> str:
|
|
214
|
+
if requested in LEGACY_PROTOCOL_VERSIONS:
|
|
199
215
|
return requested
|
|
200
|
-
return
|
|
216
|
+
return LATEST_LEGACY_PROTOCOL_VERSION
|
|
201
217
|
|
|
202
218
|
|
|
203
219
|
def handle_initialize_request(message: JsonRpcRequest) -> dict[str, Any]:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
Negotiates the protocol version with the client. If the client requests
|
|
207
|
-
a supported version, the server echoes it back. Otherwise the server
|
|
208
|
-
responds with the latest version it supports and the client may disconnect
|
|
209
|
-
if it cannot work with that version.
|
|
210
|
-
|
|
211
|
-
Args:
|
|
212
|
-
message: The JSON-RPC request message
|
|
213
|
-
|
|
214
|
-
Returns:
|
|
215
|
-
Response with negotiated protocol details
|
|
216
|
-
"""
|
|
217
|
-
params = message.get("params", {})
|
|
218
|
-
requested = params.get("protocolVersion", LATEST_PROTOCOL_VERSION)
|
|
219
|
-
negotiated = _negotiate_protocol_version(requested)
|
|
220
|
-
|
|
220
|
+
params = _protocol.params_of(message)
|
|
221
|
+
requested = params.get("protocolVersion", LATEST_LEGACY_PROTOCOL_VERSION)
|
|
221
222
|
return {
|
|
222
|
-
"
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"tools": {
|
|
226
|
-
"listChanged": False,
|
|
227
|
-
}
|
|
228
|
-
},
|
|
229
|
-
"serverInfo": {"name": "LangGraph", "version": __version__},
|
|
230
|
-
}
|
|
223
|
+
"protocolVersion": _negotiate_protocol_version(requested),
|
|
224
|
+
"capabilities": _protocol.SERVER_CAPABILITIES,
|
|
225
|
+
"serverInfo": _protocol.SERVER_INFO,
|
|
231
226
|
}
|
|
232
227
|
|
|
233
228
|
|
|
234
229
|
async def handle_tools_list(
|
|
235
230
|
request: ApiRequest, params: dict[str, Any]
|
|
236
231
|
) -> dict[str, Any]:
|
|
237
|
-
"""Handle tools/list request to get available assistants as tools.
|
|
238
|
-
|
|
239
|
-
Args:
|
|
240
|
-
request: The incoming request object. Used for propagating any headers
|
|
241
|
-
for authentication purposes.
|
|
242
|
-
params: The parameters for the tools/list request
|
|
243
|
-
|
|
244
|
-
Returns:
|
|
245
|
-
Dictionary containing list of available tools
|
|
246
|
-
"""
|
|
247
232
|
client = _client()
|
|
248
233
|
|
|
249
|
-
|
|
250
|
-
cursor = params.get("cursor", 0)
|
|
251
|
-
cursor = int(cursor)
|
|
252
|
-
except ValueError:
|
|
253
|
-
cursor = 0
|
|
234
|
+
cursor = _page_offset(params.get("cursor", 0))
|
|
254
235
|
|
|
255
|
-
# Get assistants from the API
|
|
256
|
-
# For now set a large limit to get all assistants
|
|
257
236
|
assistants = await client.assistants.search(
|
|
258
237
|
offset=cursor, limit=DEFAULT_PAGE_SIZE, headers=request.headers
|
|
259
238
|
)
|
|
@@ -263,9 +242,8 @@ async def handle_tools_list(
|
|
|
263
242
|
else:
|
|
264
243
|
next_cursor = None
|
|
265
244
|
|
|
266
|
-
# Deduplicate by normalized name, preserving original for the title field
|
|
267
245
|
seen_names: set[str] = set()
|
|
268
|
-
unique: list[tuple[Any, str]] = []
|
|
246
|
+
unique: list[tuple[Any, str]] = []
|
|
269
247
|
for assistant in assistants:
|
|
270
248
|
normalized = _sanitizers.normalize_name(assistant["name"])
|
|
271
249
|
if normalized in seen_names:
|
|
@@ -279,10 +257,6 @@ async def handle_tools_list(
|
|
|
279
257
|
unique.append((assistant, normalized))
|
|
280
258
|
|
|
281
259
|
async def _get_tool(assistant: Any, normalized_name: str) -> dict[str, Any] | None:
|
|
282
|
-
"""Get tool definition for an assistant.
|
|
283
|
-
|
|
284
|
-
Returns None if schema generation fails (e.g., non-serializable state).
|
|
285
|
-
"""
|
|
286
260
|
try:
|
|
287
261
|
schemas = await client.assistants.get_schemas(
|
|
288
262
|
assistant["assistant_id"], headers=request.headers
|
|
@@ -292,7 +266,6 @@ async def handle_tools_list(
|
|
|
292
266
|
raise ValueError("Input schema generation failed")
|
|
293
267
|
if input_schema:
|
|
294
268
|
_sanitizers.simplify_mcp_schema_inplace(input_schema)
|
|
295
|
-
# MCP spec requires inputSchema to be a JSON Schema with type "object"
|
|
296
269
|
input_schema.setdefault("type", "object")
|
|
297
270
|
return {
|
|
298
271
|
"name": normalized_name,
|
|
@@ -317,99 +290,76 @@ async def handle_tools_list(
|
|
|
317
290
|
return None
|
|
318
291
|
|
|
319
292
|
tools_or_none = await asyncio.gather(*(_get_tool(a, n) for a, n in unique))
|
|
320
|
-
# Filter out assistants that failed schema generation
|
|
321
293
|
tools = [t for t in tools_or_none if t is not None]
|
|
322
294
|
|
|
323
295
|
result = {"tools": tools}
|
|
324
|
-
|
|
325
296
|
if next_cursor is not None:
|
|
326
|
-
result["nextCursor"] = next_cursor
|
|
297
|
+
result["nextCursor"] = str(next_cursor)
|
|
298
|
+
return result
|
|
327
299
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
300
|
+
|
|
301
|
+
def _page_offset(cursor: object) -> int:
|
|
302
|
+
match cursor:
|
|
303
|
+
case int() if not isinstance(cursor, bool):
|
|
304
|
+
offset = cursor
|
|
305
|
+
case str() if _CURSOR_DIGITS.fullmatch(cursor):
|
|
306
|
+
offset = int(cursor)
|
|
307
|
+
case _:
|
|
308
|
+
raise _protocol.ProtocolError(INVALID_CURSOR_MESSAGE)
|
|
309
|
+
if not 0 <= offset <= MAX_PAGE_OFFSET:
|
|
310
|
+
raise _protocol.ProtocolError(INVALID_CURSOR_MESSAGE)
|
|
311
|
+
return offset
|
|
331
312
|
|
|
332
313
|
|
|
333
314
|
async def handle_tools_call(
|
|
334
|
-
request: ApiRequest,
|
|
315
|
+
request: ApiRequest, call: _protocol.McpCall
|
|
335
316
|
) -> dict[str, Any]:
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
request: The incoming request
|
|
340
|
-
params: The parameters for the tool call
|
|
341
|
-
|
|
342
|
-
Returns:
|
|
343
|
-
The result of the tool execution
|
|
344
|
-
"""
|
|
345
|
-
client = _client()
|
|
346
|
-
|
|
347
|
-
tool_name = params.get("name")
|
|
348
|
-
|
|
349
|
-
if not tool_name:
|
|
350
|
-
return {
|
|
351
|
-
"error": {
|
|
352
|
-
"code": ERROR_CODE_INVALID_PARAMS,
|
|
353
|
-
"message": f"Unknown tool: {tool_name}",
|
|
354
|
-
},
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
arguments = params.get("arguments", {})
|
|
358
|
-
context = params.get("context")
|
|
359
|
-
assistants = await client.assistants.search(
|
|
360
|
-
limit=MAX_ASSISTANTS, headers=request.headers
|
|
361
|
-
)
|
|
362
|
-
matching_assistant = []
|
|
363
|
-
for assistant in assistants:
|
|
364
|
-
if (
|
|
365
|
-
normalized_name := _sanitizers.normalize_name(assistant["name"])
|
|
366
|
-
) and normalized_name == tool_name:
|
|
367
|
-
matching_assistant.append(assistant)
|
|
368
|
-
|
|
369
|
-
num_assistants = len(matching_assistant)
|
|
370
|
-
|
|
371
|
-
if num_assistants == 0:
|
|
372
|
-
return {
|
|
373
|
-
"error": {
|
|
374
|
-
"code": ERROR_CODE_INVALID_PARAMS,
|
|
375
|
-
"message": f"Unknown tool: {tool_name}",
|
|
376
|
-
},
|
|
377
|
-
}
|
|
378
|
-
elif num_assistants > 1:
|
|
379
|
-
return {
|
|
380
|
-
"error": {
|
|
381
|
-
"code": ERROR_CODE_INVALID_PARAMS,
|
|
382
|
-
"message": "Multiple tools found with the same name.",
|
|
383
|
-
},
|
|
384
|
-
}
|
|
385
|
-
else:
|
|
386
|
-
tool_name = matching_assistant[0]["assistant_id"]
|
|
387
|
-
|
|
388
|
-
value = await client.runs.wait(
|
|
317
|
+
tool_call = ToolCall.of(call.params)
|
|
318
|
+
assistant_id = await _assistant_serving(request, tool_call.name)
|
|
319
|
+
value = await _client().runs.wait(
|
|
389
320
|
thread_id=None,
|
|
390
|
-
assistant_id=
|
|
391
|
-
input=arguments,
|
|
392
|
-
context=context,
|
|
321
|
+
assistant_id=assistant_id,
|
|
322
|
+
input=tool_call.arguments,
|
|
323
|
+
context=tool_call.context,
|
|
393
324
|
headers=request.headers,
|
|
394
325
|
raise_error=False,
|
|
395
326
|
)
|
|
327
|
+
return _tool_result(parse_outcome(value))
|
|
328
|
+
|
|
396
329
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
330
|
+
async def _assistant_serving(request: ApiRequest, tool_name: str) -> str:
|
|
331
|
+
assistants = await _client().assistants.search(
|
|
332
|
+
limit=MAX_ASSISTANTS, headers=request.headers
|
|
333
|
+
)
|
|
334
|
+
matching = [
|
|
335
|
+
assistant
|
|
336
|
+
for assistant in assistants
|
|
337
|
+
if _sanitizers.normalize_name(assistant["name"]) == tool_name
|
|
338
|
+
]
|
|
339
|
+
if not matching:
|
|
340
|
+
raise _protocol.ProtocolError(f"Unknown tool: {tool_name}")
|
|
341
|
+
if len(matching) > 1:
|
|
342
|
+
raise _protocol.ProtocolError("Multiple tools found with the same name.")
|
|
343
|
+
return matching[0]["assistant_id"]
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _tool_result(outcome: RunOutcome) -> dict[str, JsonValue]:
|
|
347
|
+
match outcome:
|
|
348
|
+
case Completed(output=output):
|
|
349
|
+
return {"content": [_text_content(json_dumpb(output).decode())]}
|
|
350
|
+
case Failed(message=message):
|
|
351
|
+
return {"isError": True, "content": [_text_content(message)]}
|
|
352
|
+
case Paused(interrupts=interrupts):
|
|
353
|
+
return {
|
|
401
354
|
"isError": True,
|
|
402
355
|
"content": [
|
|
403
|
-
|
|
356
|
+
_text_content(PAUSED_TOOL_MESSAGE),
|
|
357
|
+
_text_content(json_dumpb(interrupts).decode()),
|
|
404
358
|
],
|
|
405
359
|
}
|
|
406
|
-
|
|
360
|
+
case _:
|
|
361
|
+
assert_never(outcome)
|
|
407
362
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
"content": [
|
|
412
|
-
{"type": "text", "text": repr(value)},
|
|
413
|
-
]
|
|
414
|
-
}
|
|
415
|
-
}
|
|
363
|
+
|
|
364
|
+
def _text_content(text: str) -> dict[str, str]:
|
|
365
|
+
return {"type": "text", "text": text}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from langgraph_api.api.mcp._constants import RUN_FAILED_MESSAGE
|
|
4
|
+
from langgraph_api.cache import JsonValue
|
|
5
|
+
from langgraph_api.schema import Interrupt
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class Completed:
|
|
10
|
+
output: JsonValue
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class Failed:
|
|
15
|
+
message: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class Paused:
|
|
20
|
+
interrupts: tuple[Interrupt, ...]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
RunOutcome = Completed | Failed | Paused
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_outcome(value: JsonValue) -> RunOutcome:
|
|
27
|
+
match value:
|
|
28
|
+
case {"__error__": error, **rest} if not rest:
|
|
29
|
+
return Failed(_error_message(error))
|
|
30
|
+
case {"__interrupt__": [_, *_] as interrupts}:
|
|
31
|
+
return Paused(tuple(interrupts))
|
|
32
|
+
case _:
|
|
33
|
+
return Completed(value)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _error_message(error: JsonValue) -> str:
|
|
37
|
+
match error:
|
|
38
|
+
case {"error": str() as name, "message": str() as message} if message:
|
|
39
|
+
return f"{name}: {message}"
|
|
40
|
+
case {"error": str() as name}:
|
|
41
|
+
return name
|
|
42
|
+
case _:
|
|
43
|
+
return RUN_FAILED_MESSAGE
|