langgraph-api 0.0.32__tar.gz → 0.0.33__tar.gz
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.
Potentially problematic release.
This version of langgraph-api might be problematic. Click here for more details.
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/PKG-INFO +1 -1
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/api/__init__.py +6 -0
- langgraph_api-0.0.33/langgraph_api/api/mcp.py +467 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/asyncio.py +21 -2
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/cli.py +1 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/config.py +5 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/graph.py +24 -8
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/lifespan.py +10 -1
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/logging.py +11 -10
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/metadata.py +1 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/models/run.py +11 -1
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/stream.py +2 -1
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/worker.py +2 -2
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/pyproject.toml +1 -1
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/LICENSE +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/README.md +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/__init__.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/api/assistants.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/api/meta.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/api/openapi.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/api/runs.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/api/store.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/api/threads.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/api/ui.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/auth/__init__.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/auth/custom.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/auth/langsmith/__init__.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/auth/langsmith/backend.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/auth/langsmith/client.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/auth/middleware.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/auth/noop.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/auth/studio_user.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/command.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/cron_scheduler.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/errors.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/http.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/.gitignore +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/base.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/build.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/client.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/errors.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/global.d.ts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/package.json +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/remote.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/schema.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/src/graph.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/src/hooks.mjs +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/src/parser/parser.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/src/parser/parser.worker.mjs +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/src/schema/types.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/src/schema/types.template.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/src/utils/importMap.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/src/utils/serde.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/sse.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/tests/api.test.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/tests/compose-postgres.yml +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/tests/graphs/.gitignore +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/tests/graphs/agent.css +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/tests/graphs/agent.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/tests/graphs/agent.ui.tsx +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/tests/graphs/delay.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/tests/graphs/error.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/tests/graphs/langgraph.json +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/tests/graphs/nested.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/tests/graphs/package.json +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/tests/graphs/weather.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/tests/graphs/yarn.lock +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/tests/parser.test.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/tests/utils.mts +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/yarn.lock +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/middleware/__init__.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/middleware/http_logger.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/middleware/private_network.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/models/__init__.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/patch.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/queue_entrypoint.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/route.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/schema.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/serde.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/server.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/sse.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/state.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/utils.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/validation.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/webhook.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_license/__init__.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_license/middleware.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_license/validation.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_storage/__init__.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_storage/checkpoint.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_storage/database.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_storage/inmem_stream.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_storage/ops.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_storage/queue.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_storage/retry.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_storage/store.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_storage/ttl_dict.py +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/logging.json +0 -0
- {langgraph_api-0.0.32 → langgraph_api-0.0.33}/openapi.json +0 -0
|
@@ -10,6 +10,7 @@ from starlette.responses import HTMLResponse, JSONResponse, Response
|
|
|
10
10
|
from starlette.routing import BaseRoute, Mount, Route
|
|
11
11
|
|
|
12
12
|
from langgraph_api.api.assistants import assistants_routes
|
|
13
|
+
from langgraph_api.api.mcp import mcp_routes
|
|
13
14
|
from langgraph_api.api.meta import meta_info, meta_metrics
|
|
14
15
|
from langgraph_api.api.openapi import get_openapi_spec
|
|
15
16
|
from langgraph_api.api.runs import runs_routes
|
|
@@ -66,6 +67,11 @@ if HTTP_CONFIG:
|
|
|
66
67
|
protected_routes.extend(store_routes)
|
|
67
68
|
if not HTTP_CONFIG.get("disable_ui"):
|
|
68
69
|
protected_routes.extend(ui_routes)
|
|
70
|
+
# Default for disabling MCP. Until we can verify that the protocol is working
|
|
71
|
+
# correctly. This is dependent on the release of an official MCP client
|
|
72
|
+
# implementation.
|
|
73
|
+
if not HTTP_CONFIG.get("disable_mcp", True):
|
|
74
|
+
protected_routes.extend(mcp_routes)
|
|
69
75
|
else:
|
|
70
76
|
protected_routes.extend(assistants_routes)
|
|
71
77
|
protected_routes.extend(runs_routes)
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"""Implement MCP endpoint for Streamable HTTP protocol.
|
|
2
|
+
|
|
3
|
+
The current version of the RFC can be found here:
|
|
4
|
+
|
|
5
|
+
https://github.com/modelcontextprotocol/specification/blob/0f4924b07447073cbe1e29fbe64e42d379b52b04/docs/specification/draft/basic/transports.md#streamable-http
|
|
6
|
+
|
|
7
|
+
Tools specification:
|
|
8
|
+
|
|
9
|
+
https://github.com/modelcontextprotocol/specification/blob/0f4924b07447073cbe1e29fbe64e42d379b52b04/docs/specification/draft/server/tools.md
|
|
10
|
+
|
|
11
|
+
Message format:
|
|
12
|
+
|
|
13
|
+
https://github.com/modelcontextprotocol/specification/blob/0f4924b07447073cbe1e29fbe64e42d379b52b04/docs/specification/draft/basic/messages.md
|
|
14
|
+
|
|
15
|
+
Error handling with tools:
|
|
16
|
+
|
|
17
|
+
https://github.com/modelcontextprotocol/specification/blob/0f4924b07447073cbe1e29fbe64e42d379b52b04/docs/specification/draft/server/tools.md#error-handling
|
|
18
|
+
|
|
19
|
+
Streamable HTTP is a protocol that allows for the use of HTTP as transport.
|
|
20
|
+
|
|
21
|
+
The protocol supports both stateless and stateful interactions, and allows
|
|
22
|
+
the server to respond via either Application/JSON or text/event-stream.
|
|
23
|
+
|
|
24
|
+
LangGraph's implementation is currently stateless and only uses Application/JSON.
|
|
25
|
+
|
|
26
|
+
1. Adding stateful sessions: A stateful session would in theory allow agents used
|
|
27
|
+
as tools to remember past interactions. We likely do not want to map a session
|
|
28
|
+
to a thread ID as a single session may involve more than one tool call.
|
|
29
|
+
We would need to map a session to a collection of threads.
|
|
30
|
+
|
|
31
|
+
2. text/event-stream (SSE): Should be simple to add we'd want to make sure
|
|
32
|
+
we know what information we want to stream; e.g., progress notifications or
|
|
33
|
+
custom notifications.
|
|
34
|
+
|
|
35
|
+
In addition, the server could support resumability by allowing clients to specify
|
|
36
|
+
a Last-Event-ID in the request headers.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
import functools
|
|
40
|
+
import json
|
|
41
|
+
from typing import Any, NotRequired, cast
|
|
42
|
+
|
|
43
|
+
from langgraph_sdk.client import LangGraphClient, get_client
|
|
44
|
+
from starlette.responses import JSONResponse, Response
|
|
45
|
+
from structlog import getLogger
|
|
46
|
+
from typing_extensions import TypedDict
|
|
47
|
+
|
|
48
|
+
from langgraph_api.route import ApiRequest, ApiRoute
|
|
49
|
+
|
|
50
|
+
logger = getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class JsonRpcErrorObject(TypedDict):
|
|
54
|
+
code: int
|
|
55
|
+
message: str
|
|
56
|
+
data: NotRequired[Any]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class JsonRpcRequest(TypedDict):
|
|
60
|
+
jsonrpc: str # Must be "2.0"
|
|
61
|
+
id: str | int
|
|
62
|
+
method: str
|
|
63
|
+
params: NotRequired[dict[str, Any]]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class JsonRpcResponse(TypedDict):
|
|
67
|
+
jsonrpc: str # Must be "2.0"
|
|
68
|
+
id: str | int
|
|
69
|
+
result: NotRequired[dict[str, Any]]
|
|
70
|
+
error: NotRequired[JsonRpcErrorObject]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class JsonRpcNotification(TypedDict):
|
|
74
|
+
jsonrpc: str # Must be "2.0"
|
|
75
|
+
method: str
|
|
76
|
+
params: NotRequired[dict[str, Any]]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@functools.lru_cache(maxsize=1)
|
|
80
|
+
def _client() -> LangGraphClient:
|
|
81
|
+
"""Get a client for local operations."""
|
|
82
|
+
return get_client(url=None)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Workaround assistant name not exposed in the Assistants.search API
|
|
86
|
+
MAX_ASSISTANTS = 1000
|
|
87
|
+
DEFAULT_PAGE_SIZE = 100
|
|
88
|
+
|
|
89
|
+
# JSON-RPC error codes: https://www.jsonrpc.org/specification#error_object
|
|
90
|
+
ERROR_CODE_INVALID_PARAMS = -32602
|
|
91
|
+
ERROR_CODE_METHOD_NOT_FOUND = -32601
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def handle_mcp_endpoint(request: ApiRequest) -> Response:
|
|
95
|
+
"""MCP endpoint handler the implements the Streamable HTTP protocol.
|
|
96
|
+
|
|
97
|
+
The handler is expected to support the following methods:
|
|
98
|
+
|
|
99
|
+
- POST: Process a JSON-RPC request
|
|
100
|
+
- DELETE: Terminate a session
|
|
101
|
+
|
|
102
|
+
We currently do not support:
|
|
103
|
+
- /GET (initiates a streaming session)
|
|
104
|
+
This endpoint can be used to RESUME a previously interrupted session.
|
|
105
|
+
- text/event-stream (streaming) response from the server.
|
|
106
|
+
|
|
107
|
+
Support for these can be added, we just need to determine what information
|
|
108
|
+
from the agent we want to stream.
|
|
109
|
+
|
|
110
|
+
One possibility is to map "custom" stream mode to server side notifications.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
request: The incoming request object
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The response to the request
|
|
117
|
+
"""
|
|
118
|
+
# Route request based on HTTP method
|
|
119
|
+
if request.method == "DELETE":
|
|
120
|
+
return handle_delete_request()
|
|
121
|
+
elif request.method == "GET":
|
|
122
|
+
return handle_get_request()
|
|
123
|
+
elif request.method == "POST":
|
|
124
|
+
return await handle_post_request(request)
|
|
125
|
+
else:
|
|
126
|
+
# Method not allowed
|
|
127
|
+
return Response(status_code=405)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def handle_delete_request() -> Response:
|
|
131
|
+
"""Handle HTTP DELETE requests for session termination.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Response with appropriate status code
|
|
135
|
+
"""
|
|
136
|
+
return Response(status_code=404)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def handle_get_request() -> Response:
|
|
140
|
+
"""Handle HTTP GET requests for streaming (not currently supported).
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Method not allowed response
|
|
144
|
+
"""
|
|
145
|
+
# Does not support streaming at the moment
|
|
146
|
+
return Response(status_code=405)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def handle_post_request(request: ApiRequest) -> Response:
|
|
150
|
+
"""Handle HTTP POST requests for JSON-RPC messaging.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
request: The incoming request object
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Response to the JSON-RPC message
|
|
157
|
+
"""
|
|
158
|
+
body = await request.body()
|
|
159
|
+
|
|
160
|
+
# Validate JSON
|
|
161
|
+
try:
|
|
162
|
+
message = json.loads(body)
|
|
163
|
+
except json.JSONDecodeError:
|
|
164
|
+
return create_error_response("Invalid JSON", 400)
|
|
165
|
+
|
|
166
|
+
# Validate Accept header
|
|
167
|
+
if not is_valid_accept_header(request):
|
|
168
|
+
return create_error_response(
|
|
169
|
+
"Accept header must include application/json or text/event-stream", 400
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Validate message format
|
|
173
|
+
if not isinstance(message, dict):
|
|
174
|
+
return create_error_response("Invalid message format.", 400)
|
|
175
|
+
|
|
176
|
+
# Determine message type and route to appropriate handler
|
|
177
|
+
id_ = message.get("id")
|
|
178
|
+
method = message.get("method")
|
|
179
|
+
|
|
180
|
+
# Check for required jsonrpc field
|
|
181
|
+
if message.get("jsonrpc") != "2.0":
|
|
182
|
+
return create_error_response(
|
|
183
|
+
"Invalid JSON-RPC message. Missing or invalid jsonrpc version.", 400
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if id_ and method:
|
|
187
|
+
# JSON-RPC request
|
|
188
|
+
return await handle_jsonrpc_request(request, cast(JsonRpcRequest, message))
|
|
189
|
+
elif id_:
|
|
190
|
+
# JSON-RPC response
|
|
191
|
+
return handle_jsonrpc_response(cast(JsonRpcResponse, message))
|
|
192
|
+
elif method:
|
|
193
|
+
# JSON-RPC notification
|
|
194
|
+
return handle_jsonrpc_notification(cast(JsonRpcNotification, message))
|
|
195
|
+
else:
|
|
196
|
+
# Invalid message format
|
|
197
|
+
return create_error_response(
|
|
198
|
+
"Invalid message format. A message is to be either a JSON-RPC "
|
|
199
|
+
"request, response, or notification."
|
|
200
|
+
"Please see the Messages section of the Streamable HTTP RFC "
|
|
201
|
+
"for more information.",
|
|
202
|
+
400,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def is_valid_accept_header(request: ApiRequest) -> bool:
|
|
207
|
+
"""Check if the Accept header contains supported content types.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
request: The incoming request
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
True if header contains application/json or text/event-stream
|
|
214
|
+
"""
|
|
215
|
+
accept_header = request.headers.get("Accept", "")
|
|
216
|
+
accepts_json = "application/json" in accept_header
|
|
217
|
+
accepts_sse = "text/event-stream" in accept_header
|
|
218
|
+
return accepts_json or accepts_sse
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def create_error_response(message: str, status_code: int) -> Response:
|
|
222
|
+
"""Create a JSON error response.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
message: The error message
|
|
226
|
+
status_code: The HTTP status code
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
JSON response with error details
|
|
230
|
+
"""
|
|
231
|
+
return Response(
|
|
232
|
+
content=json.dumps({"error": message}),
|
|
233
|
+
status_code=status_code,
|
|
234
|
+
media_type="application/json",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
async def handle_jsonrpc_request(
|
|
239
|
+
request: ApiRequest,
|
|
240
|
+
message: JsonRpcRequest,
|
|
241
|
+
) -> Response:
|
|
242
|
+
"""Handle JSON-RPC requests (messages with both id and method).
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
request: The incoming request object
|
|
246
|
+
message: The parsed JSON-RPC message
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Response to the request
|
|
250
|
+
"""
|
|
251
|
+
method = message["method"]
|
|
252
|
+
params = message.get("params", {})
|
|
253
|
+
|
|
254
|
+
if method == "initialize":
|
|
255
|
+
result_or_error = handle_initialize_request(message)
|
|
256
|
+
elif method == "tools/list":
|
|
257
|
+
result_or_error = await handle_tools_list(request, params)
|
|
258
|
+
elif method == "tools/call":
|
|
259
|
+
result_or_error = await handle_tools_call(request, params)
|
|
260
|
+
else:
|
|
261
|
+
result_or_error = {
|
|
262
|
+
"error": {
|
|
263
|
+
"code": ERROR_CODE_METHOD_NOT_FOUND,
|
|
264
|
+
"message": f"Method not found: {method}",
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
# Process the result or error output
|
|
269
|
+
exists = {"error", "result"} - set(result_or_error.keys())
|
|
270
|
+
if len(exists) != 1:
|
|
271
|
+
raise AssertionError(
|
|
272
|
+
"Internal server error. Invalid response in MCP protocol implementation."
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return JSONResponse(
|
|
276
|
+
{
|
|
277
|
+
"jsonrpc": "2.0",
|
|
278
|
+
"id": message["id"],
|
|
279
|
+
**result_or_error,
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def handle_initialize_request(message: JsonRpcRequest) -> dict[str, Any]:
|
|
285
|
+
"""Handle initialize requests to create a new session.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
message: The JSON-RPC request message
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Response with new session details
|
|
292
|
+
"""
|
|
293
|
+
return {
|
|
294
|
+
"result": {
|
|
295
|
+
# We do not return a session ID right now.
|
|
296
|
+
"capabilities": {
|
|
297
|
+
"tools": {
|
|
298
|
+
# We do not support subscriptions currently
|
|
299
|
+
"listChanged": False,
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def handle_jsonrpc_response(message: JsonRpcResponse) -> Response:
|
|
307
|
+
"""Handle JSON-RPC responses (messages with id but no method).
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
message: The parsed JSON-RPC response message
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Acknowledgement response
|
|
314
|
+
"""
|
|
315
|
+
# For any responses, we just acknowledge receipt
|
|
316
|
+
return Response(status_code=202)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def handle_jsonrpc_notification(message: JsonRpcNotification) -> Response:
|
|
320
|
+
"""Handle JSON-RPC notifications (messages with method but no id).
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
message: The parsed JSON-RPC message
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Response to the notification
|
|
327
|
+
"""
|
|
328
|
+
return Response(status_code=202)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
async def handle_tools_list(
|
|
332
|
+
request: ApiRequest, params: dict[str, Any]
|
|
333
|
+
) -> dict[str, Any]:
|
|
334
|
+
"""Handle tools/list request to get available assistants as tools.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
request: The incoming request object. Used for propagating any headers
|
|
338
|
+
for authentication purposes.
|
|
339
|
+
params: The parameters for the tools/list request
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
Dictionary containing list of available tools
|
|
343
|
+
"""
|
|
344
|
+
client = _client()
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
cursor = params.get("cursor", 0)
|
|
348
|
+
cursor = int(cursor)
|
|
349
|
+
except ValueError:
|
|
350
|
+
cursor = 0
|
|
351
|
+
|
|
352
|
+
# Get assistants from the API
|
|
353
|
+
# For now set a large limit to get all assistants
|
|
354
|
+
assistants = await client.assistants.search(offset=cursor, limit=DEFAULT_PAGE_SIZE)
|
|
355
|
+
|
|
356
|
+
if len(assistants) == DEFAULT_PAGE_SIZE:
|
|
357
|
+
next_cursor = cursor + DEFAULT_PAGE_SIZE
|
|
358
|
+
else:
|
|
359
|
+
next_cursor = None
|
|
360
|
+
|
|
361
|
+
# Format assistants as tools for MCP
|
|
362
|
+
tools = []
|
|
363
|
+
seen_names = set()
|
|
364
|
+
for assistant in assistants:
|
|
365
|
+
id_ = assistant.get("assistant_id")
|
|
366
|
+
name = assistant["name"]
|
|
367
|
+
|
|
368
|
+
if name in seen_names:
|
|
369
|
+
await logger.awarning(f"Duplicate assistant name found {name}", name=name)
|
|
370
|
+
else:
|
|
371
|
+
seen_names.add(name)
|
|
372
|
+
|
|
373
|
+
schemas = await client.assistants.get_schemas(id_)
|
|
374
|
+
tools.append(
|
|
375
|
+
{
|
|
376
|
+
"name": name,
|
|
377
|
+
"inputSchema": schemas.get("input_schema", {}),
|
|
378
|
+
"description": "",
|
|
379
|
+
},
|
|
380
|
+
)
|
|
381
|
+
return {"result": {"tools": tools, "nextCursor": next_cursor}}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
async def handle_tools_call(
|
|
385
|
+
request: ApiRequest, params: dict[str, Any]
|
|
386
|
+
) -> dict[str, Any]:
|
|
387
|
+
"""Handle tools/call request to execute an assistant.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
request: The incoming request
|
|
391
|
+
params: The parameters for the tool call
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
The result of the tool execution
|
|
395
|
+
"""
|
|
396
|
+
client = _client()
|
|
397
|
+
|
|
398
|
+
tool_name = params.get("name")
|
|
399
|
+
|
|
400
|
+
if not tool_name:
|
|
401
|
+
return {
|
|
402
|
+
"jsonrpc": "2.0",
|
|
403
|
+
"id": 3,
|
|
404
|
+
"error": {
|
|
405
|
+
"code": ERROR_CODE_INVALID_PARAMS,
|
|
406
|
+
"message": f"Unknown tool: {tool_name}",
|
|
407
|
+
},
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
arguments = params.get("arguments", {})
|
|
411
|
+
assistants = await client.assistants.search(limit=MAX_ASSISTANTS)
|
|
412
|
+
matching_assistant = [
|
|
413
|
+
assistant for assistant in assistants if assistant["name"] == tool_name
|
|
414
|
+
]
|
|
415
|
+
|
|
416
|
+
num_assistants = len(matching_assistant)
|
|
417
|
+
|
|
418
|
+
if num_assistants == 0:
|
|
419
|
+
return {
|
|
420
|
+
"jsonrpc": "2.0",
|
|
421
|
+
"id": 3,
|
|
422
|
+
"error": {
|
|
423
|
+
"code": ERROR_CODE_INVALID_PARAMS,
|
|
424
|
+
"message": f"Unknown tool: {tool_name}",
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
elif num_assistants > 1:
|
|
428
|
+
return {
|
|
429
|
+
"jsonrpc": "2.0",
|
|
430
|
+
"id": 3,
|
|
431
|
+
"error": {
|
|
432
|
+
"code": ERROR_CODE_INVALID_PARAMS,
|
|
433
|
+
"message": "Multiple tools found with the same name.",
|
|
434
|
+
},
|
|
435
|
+
}
|
|
436
|
+
else:
|
|
437
|
+
tool_name = matching_assistant[0]["assistant_id"]
|
|
438
|
+
|
|
439
|
+
value = await client.runs.wait(
|
|
440
|
+
thread_id=None, assistant_id=tool_name, input=arguments, raise_error=False
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
if "__error__" in value:
|
|
444
|
+
# This is a run-time error in the tool.
|
|
445
|
+
return {
|
|
446
|
+
"result": {
|
|
447
|
+
"isError": True,
|
|
448
|
+
"content": [
|
|
449
|
+
{"type": "text", "value": value["__error__"]["error"]},
|
|
450
|
+
],
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
# All good, return the result
|
|
455
|
+
return {
|
|
456
|
+
"result": {
|
|
457
|
+
"content": [
|
|
458
|
+
{"type": "text", "value": repr(value)},
|
|
459
|
+
]
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# Define routes for the MCP endpoint
|
|
465
|
+
mcp_routes = [
|
|
466
|
+
ApiRoute("/mcp", handle_mcp_endpoint, methods=["GET", "POST", "DELETE"]),
|
|
467
|
+
]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import concurrent.futures
|
|
2
3
|
from collections.abc import AsyncIterator, Coroutine
|
|
3
4
|
from contextlib import AbstractAsyncContextManager
|
|
4
5
|
from functools import partial
|
|
@@ -10,6 +11,13 @@ T = TypeVar("T")
|
|
|
10
11
|
|
|
11
12
|
logger = structlog.stdlib.get_logger(__name__)
|
|
12
13
|
|
|
14
|
+
_MAIN_LOOP: asyncio.AbstractEventLoop | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def set_event_loop(loop: asyncio.AbstractEventLoop) -> None:
|
|
18
|
+
global _MAIN_LOOP
|
|
19
|
+
_MAIN_LOOP = loop
|
|
20
|
+
|
|
13
21
|
|
|
14
22
|
async def sleep_if_not_done(delay: float, done: asyncio.Event) -> None:
|
|
15
23
|
try:
|
|
@@ -76,9 +84,10 @@ PENDING_TASKS = set()
|
|
|
76
84
|
|
|
77
85
|
|
|
78
86
|
def _create_task_done_callback(
|
|
79
|
-
ignore_exceptions: tuple[Exception, ...],
|
|
87
|
+
ignore_exceptions: tuple[Exception, ...],
|
|
88
|
+
task: asyncio.Task | asyncio.Future,
|
|
80
89
|
) -> None:
|
|
81
|
-
PENDING_TASKS.
|
|
90
|
+
PENDING_TASKS.discard(task)
|
|
82
91
|
try:
|
|
83
92
|
if exc := task.exception():
|
|
84
93
|
if not isinstance(exc, ignore_exceptions):
|
|
@@ -97,6 +106,16 @@ def create_task(
|
|
|
97
106
|
return task
|
|
98
107
|
|
|
99
108
|
|
|
109
|
+
def run_coroutine_threadsafe(
|
|
110
|
+
coro: Coroutine[Any, Any, T], ignore_exceptions: tuple[type[Exception], ...] = ()
|
|
111
|
+
) -> concurrent.futures.Future[T | None]:
|
|
112
|
+
if _MAIN_LOOP is None:
|
|
113
|
+
raise RuntimeError("No event loop set")
|
|
114
|
+
future = asyncio.run_coroutine_threadsafe(coro, _MAIN_LOOP)
|
|
115
|
+
future.add_done_callback(partial(_create_task_done_callback, ignore_exceptions))
|
|
116
|
+
return future
|
|
117
|
+
|
|
118
|
+
|
|
100
119
|
class SimpleTaskGroup(AbstractAsyncContextManager["SimpleTaskGroup"]):
|
|
101
120
|
"""An async task group that can be configured to wait and/or cancel tasks on exit.
|
|
102
121
|
|
|
@@ -189,6 +189,7 @@ def run_server(
|
|
|
189
189
|
LANGSMITH_LANGGRAPH_API_VARIANT="local_dev",
|
|
190
190
|
LANGGRAPH_AUTH=json.dumps(auth) if auth else None,
|
|
191
191
|
LANGGRAPH_HTTP=json.dumps(http) if http else None,
|
|
192
|
+
LANGGRAPH_API_URL=local_url,
|
|
192
193
|
# See https://developer.chrome.com/blog/private-network-access-update-2024-03
|
|
193
194
|
ALLOW_PRIVATE_NETWORK="true",
|
|
194
195
|
**(env_vars or {}),
|
|
@@ -32,6 +32,11 @@ class HttpConfig(TypedDict, total=False):
|
|
|
32
32
|
disable_meta: bool
|
|
33
33
|
"""Disable /ok, /info, /metrics, and /docs routes"""
|
|
34
34
|
cors: CorsConfig | None
|
|
35
|
+
"""CORS configuration"""
|
|
36
|
+
disable_ui: bool
|
|
37
|
+
"""Disable /ui routes"""
|
|
38
|
+
disable_mcp: bool
|
|
39
|
+
"""Disable /mcp routes"""
|
|
35
40
|
|
|
36
41
|
|
|
37
42
|
class IndexConfig(TypedDict, total=False):
|
|
@@ -21,6 +21,7 @@ from langgraph.pregel import Pregel
|
|
|
21
21
|
from langgraph.store.base import BaseStore
|
|
22
22
|
from starlette.exceptions import HTTPException
|
|
23
23
|
|
|
24
|
+
from langgraph_api import asyncio as lg_asyncio
|
|
24
25
|
from langgraph_api.js.base import BaseRemotePregel
|
|
25
26
|
from langgraph_api.schema import Config
|
|
26
27
|
|
|
@@ -60,6 +61,12 @@ async def register_graph(graph_id: str, graph: GraphValue, config: dict | None)
|
|
|
60
61
|
)
|
|
61
62
|
|
|
62
63
|
|
|
64
|
+
def register_graph_sync(
|
|
65
|
+
graph_id: str, graph: GraphValue, config: dict | None = None
|
|
66
|
+
) -> None:
|
|
67
|
+
lg_asyncio.run_coroutine_threadsafe(register_graph(graph_id, graph, config))
|
|
68
|
+
|
|
69
|
+
|
|
63
70
|
@asynccontextmanager
|
|
64
71
|
async def _generate_graph(value: Any) -> AsyncIterator[Any]:
|
|
65
72
|
"""Yield a graph object regardless of its type."""
|
|
@@ -187,24 +194,33 @@ async def collect_graphs_from_env(register: bool = False) -> None:
|
|
|
187
194
|
config_per_graph = _load_graph_config_from_env() or {}
|
|
188
195
|
|
|
189
196
|
if paths_str:
|
|
190
|
-
specs = [
|
|
191
|
-
|
|
197
|
+
specs = []
|
|
198
|
+
for key, value in json.loads(paths_str).items():
|
|
199
|
+
try:
|
|
200
|
+
path_or_module, variable = value.rsplit(":", maxsplit=1)
|
|
201
|
+
except ValueError as e:
|
|
202
|
+
raise ValueError(
|
|
203
|
+
f"Invalid path '{value}' for graph '{key}'."
|
|
204
|
+
" Did you miss a variable name?\n"
|
|
205
|
+
" Expected one of the following formats:"
|
|
206
|
+
" 'my.module:variable_name' or '/path/to/file.py:variable_name'"
|
|
207
|
+
) from e
|
|
208
|
+
specs.append(
|
|
192
209
|
GraphSpec(
|
|
193
210
|
key,
|
|
194
|
-
module=
|
|
195
|
-
variable=
|
|
211
|
+
module=path_or_module,
|
|
212
|
+
variable=variable,
|
|
196
213
|
config=config_per_graph.get(key),
|
|
197
214
|
)
|
|
198
215
|
if "/" not in value
|
|
199
216
|
else GraphSpec(
|
|
200
217
|
key,
|
|
201
|
-
path=
|
|
202
|
-
variable=
|
|
218
|
+
path=path_or_module,
|
|
219
|
+
variable=variable,
|
|
203
220
|
config=config_per_graph.get(key),
|
|
204
221
|
)
|
|
205
222
|
)
|
|
206
|
-
|
|
207
|
-
]
|
|
223
|
+
|
|
208
224
|
else:
|
|
209
225
|
specs = [
|
|
210
226
|
GraphSpec(
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from contextlib import asynccontextmanager
|
|
3
3
|
|
|
4
|
+
import structlog
|
|
4
5
|
from starlette.applications import Starlette
|
|
5
6
|
|
|
6
7
|
import langgraph_api.config as config
|
|
7
|
-
from langgraph_api.asyncio import SimpleTaskGroup
|
|
8
|
+
from langgraph_api.asyncio import SimpleTaskGroup, set_event_loop
|
|
8
9
|
from langgraph_api.cron_scheduler import cron_scheduler
|
|
9
10
|
from langgraph_api.graph import collect_graphs_from_env, stop_remote_graphs
|
|
10
11
|
from langgraph_api.http import start_http_client, stop_http_client
|
|
@@ -14,6 +15,8 @@ from langgraph_storage.database import start_pool, stop_pool
|
|
|
14
15
|
from langgraph_storage.queue import queue
|
|
15
16
|
from langgraph_storage.store import Store
|
|
16
17
|
|
|
18
|
+
logger = structlog.stdlib.get_logger(__name__)
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
@asynccontextmanager
|
|
19
22
|
async def lifespan(
|
|
@@ -21,6 +24,12 @@ async def lifespan(
|
|
|
21
24
|
with_cron_scheduler: bool = True,
|
|
22
25
|
taskset: set[asyncio.Task] | None = None,
|
|
23
26
|
):
|
|
27
|
+
try:
|
|
28
|
+
current_loop = asyncio.get_running_loop()
|
|
29
|
+
set_event_loop(current_loop)
|
|
30
|
+
except RuntimeError:
|
|
31
|
+
await logger.aerror("Failed to set loop")
|
|
32
|
+
|
|
24
33
|
if not await get_license_status():
|
|
25
34
|
raise ValueError(
|
|
26
35
|
"License verification failed. Please ensure proper configuration:\n"
|
|
@@ -119,13 +119,14 @@ class Formatter(structlog.stdlib.ProcessorFormatter):
|
|
|
119
119
|
|
|
120
120
|
# configure structlog
|
|
121
121
|
|
|
122
|
-
structlog.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
122
|
+
if not structlog.is_configured():
|
|
123
|
+
structlog.configure(
|
|
124
|
+
processors=[
|
|
125
|
+
structlog.stdlib.filter_by_level,
|
|
126
|
+
*shared_processors,
|
|
127
|
+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
128
|
+
],
|
|
129
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
130
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
131
|
+
cache_logger_on_first_use=True,
|
|
132
|
+
)
|
|
@@ -241,7 +241,17 @@ async def create_valid_run(
|
|
|
241
241
|
# handle multitask strategy
|
|
242
242
|
inflight_runs = [run async for run in run_]
|
|
243
243
|
if first["run_id"] == run_id:
|
|
244
|
-
logger.info(
|
|
244
|
+
logger.info(
|
|
245
|
+
"Created run",
|
|
246
|
+
run_id=str(run_id),
|
|
247
|
+
thread_id=str(thread_id),
|
|
248
|
+
assistant_id=str(assistant_id),
|
|
249
|
+
multitask_strategy=multitask_strategy,
|
|
250
|
+
stream_mode=stream_mode,
|
|
251
|
+
temporary=temporary,
|
|
252
|
+
after_seconds=payload.get("after_seconds", 0),
|
|
253
|
+
if_not_exists=payload.get("if_not_exists", "reject"),
|
|
254
|
+
)
|
|
245
255
|
# inserted, proceed
|
|
246
256
|
if multitask_strategy in ("interrupt", "rollback") and inflight_runs:
|
|
247
257
|
try:
|
|
@@ -26,7 +26,7 @@ from langgraph_api.asyncio import ValueEvent, wait_if_not_done
|
|
|
26
26
|
from langgraph_api.command import map_cmd
|
|
27
27
|
from langgraph_api.graph import get_graph
|
|
28
28
|
from langgraph_api.js.base import BaseRemotePregel
|
|
29
|
-
from langgraph_api.metadata import HOST, PLAN, incr_nodes
|
|
29
|
+
from langgraph_api.metadata import HOST, PLAN, USER_API_URL, incr_nodes
|
|
30
30
|
from langgraph_api.schema import Run, StreamMode
|
|
31
31
|
from langgraph_api.serde import json_dumpb
|
|
32
32
|
from langgraph_api.utils import AsyncConnectionProto
|
|
@@ -114,6 +114,7 @@ async def astream_state(
|
|
|
114
114
|
config["metadata"]["langgraph_version"] = langgraph.version.__version__
|
|
115
115
|
config["metadata"]["langgraph_plan"] = PLAN
|
|
116
116
|
config["metadata"]["langgraph_host"] = HOST
|
|
117
|
+
config["metadata"]["langgraph_api_url"] = USER_API_URL
|
|
117
118
|
# attach node counter
|
|
118
119
|
if not isinstance(graph, BaseRemotePregel):
|
|
119
120
|
config["configurable"]["__pregel_node_finished"] = incr_nodes
|
|
@@ -210,7 +210,7 @@ async def worker(
|
|
|
210
210
|
status = "retry"
|
|
211
211
|
run_ended_at = datetime.now(UTC).isoformat()
|
|
212
212
|
await logger.awarning(
|
|
213
|
-
"Background run failed, will retry",
|
|
213
|
+
f"Background run failed, will retry. Exception: {e}",
|
|
214
214
|
exc_info=True,
|
|
215
215
|
run_id=str(run_id),
|
|
216
216
|
run_attempt=attempt,
|
|
@@ -226,7 +226,7 @@ async def worker(
|
|
|
226
226
|
status = "error"
|
|
227
227
|
run_ended_at = datetime.now(UTC).isoformat()
|
|
228
228
|
await logger.aexception(
|
|
229
|
-
"Background run failed",
|
|
229
|
+
f"Background run failed. Exception: {exc}",
|
|
230
230
|
exc_info=not isinstance(exc, RemoteException),
|
|
231
231
|
run_id=str(run_id),
|
|
232
232
|
run_attempt=attempt,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{langgraph_api-0.0.32 → langgraph_api-0.0.33}/langgraph_api/js/src/schema/types.template.mts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|