langgraph-api 0.2.130__py3-none-any.whl → 0.2.132__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.
Potentially problematic release.
This version of langgraph-api might be problematic. Click here for more details.
- langgraph_api/__init__.py +1 -1
- langgraph_api/api/assistants.py +6 -5
- langgraph_api/api/meta.py +3 -1
- langgraph_api/api/openapi.py +1 -1
- langgraph_api/api/runs.py +13 -10
- langgraph_api/api/ui.py +2 -0
- langgraph_api/asgi_transport.py +2 -2
- langgraph_api/asyncio.py +10 -8
- langgraph_api/auth/custom.py +9 -4
- langgraph_api/auth/langsmith/client.py +1 -1
- langgraph_api/cli.py +5 -4
- langgraph_api/config.py +1 -1
- langgraph_api/executor_entrypoint.py +23 -0
- langgraph_api/graph.py +25 -9
- langgraph_api/http.py +10 -7
- langgraph_api/http_metrics.py +4 -1
- langgraph_api/js/build.mts +11 -2
- langgraph_api/js/client.http.mts +2 -0
- langgraph_api/js/client.mts +13 -3
- langgraph_api/js/remote.py +17 -12
- langgraph_api/js/src/preload.mjs +9 -1
- langgraph_api/js/src/utils/files.mts +5 -2
- langgraph_api/js/sse.py +1 -1
- langgraph_api/logging.py +3 -3
- langgraph_api/middleware/http_logger.py +2 -1
- langgraph_api/models/run.py +19 -14
- langgraph_api/patch.py +2 -2
- langgraph_api/queue_entrypoint.py +33 -18
- langgraph_api/schema.py +20 -1
- langgraph_api/serde.py +32 -5
- langgraph_api/server.py +5 -3
- langgraph_api/state.py +8 -8
- langgraph_api/store.py +1 -1
- langgraph_api/stream.py +33 -20
- langgraph_api/traceblock.py +1 -1
- langgraph_api/utils/__init__.py +21 -5
- langgraph_api/utils/config.py +13 -4
- langgraph_api/utils/future.py +1 -1
- langgraph_api/utils/uuids.py +87 -0
- langgraph_api/webhook.py +20 -20
- langgraph_api/worker.py +8 -5
- {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.132.dist-info}/METADATA +1 -1
- {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.132.dist-info}/RECORD +46 -44
- {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.132.dist-info}/WHEEL +0 -0
- {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.132.dist-info}/entry_points.txt +0 -0
- {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.132.dist-info}/licenses/LICENSE +0 -0
langgraph_api/js/remote.py
CHANGED
|
@@ -106,6 +106,10 @@ async def _client_stream(method: str, data: dict[str, Any]):
|
|
|
106
106
|
yield sse.data
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
class NoopModel(BaseModel):
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
|
|
109
113
|
async def _client_invoke(method: str, data: dict[str, Any]):
|
|
110
114
|
graph_id = data.get("graph_id")
|
|
111
115
|
res = await _client.post(
|
|
@@ -177,13 +181,10 @@ class RemotePregel(BaseRemotePregel):
|
|
|
177
181
|
nodes: list[Any] = response.pop("nodes")
|
|
178
182
|
edges: list[Any] = response.pop("edges")
|
|
179
183
|
|
|
180
|
-
class NoopModel(BaseModel):
|
|
181
|
-
pass
|
|
182
|
-
|
|
183
184
|
return DrawableGraph(
|
|
184
185
|
{
|
|
185
186
|
data["id"]: Node(
|
|
186
|
-
data["id"], data["id"], NoopModel
|
|
187
|
+
data["id"], data["id"], NoopModel, data.get("metadata")
|
|
187
188
|
)
|
|
188
189
|
for data in nodes
|
|
189
190
|
},
|
|
@@ -244,9 +245,9 @@ class RemotePregel(BaseRemotePregel):
|
|
|
244
245
|
)
|
|
245
246
|
return tuple(result)
|
|
246
247
|
|
|
247
|
-
return StateSnapshot(
|
|
248
|
+
return StateSnapshot( # type: ignore[missing-argument]
|
|
248
249
|
item.get("values"),
|
|
249
|
-
item.get("next"),
|
|
250
|
+
cast(tuple, item.get("next", ())),
|
|
250
251
|
item.get("config"),
|
|
251
252
|
item.get("metadata"),
|
|
252
253
|
item.get("createdAt"),
|
|
@@ -352,7 +353,7 @@ class RemotePregel(BaseRemotePregel):
|
|
|
352
353
|
return result["nodesExecuted"]
|
|
353
354
|
|
|
354
355
|
|
|
355
|
-
async def run_js_process(paths_str: str, watch: bool = False):
|
|
356
|
+
async def run_js_process(paths_str: str | None, watch: bool = False):
|
|
356
357
|
# check if tsx is available
|
|
357
358
|
tsx_path = shutil.which("tsx")
|
|
358
359
|
if tsx_path is None:
|
|
@@ -360,7 +361,7 @@ async def run_js_process(paths_str: str, watch: bool = False):
|
|
|
360
361
|
"tsx not found in PATH. Please upgrade to latest LangGraph CLI to support running JS graphs."
|
|
361
362
|
)
|
|
362
363
|
attempt = 0
|
|
363
|
-
while
|
|
364
|
+
while (current_task := asyncio.current_task()) and not current_task.cancelled():
|
|
364
365
|
client_file = os.path.join(os.path.dirname(__file__), "client.mts")
|
|
365
366
|
client_preload_file = os.path.join(
|
|
366
367
|
os.path.dirname(__file__), "src", "preload.mjs"
|
|
@@ -408,7 +409,9 @@ async def run_js_process(paths_str: str, watch: bool = False):
|
|
|
408
409
|
attempt += 1
|
|
409
410
|
|
|
410
411
|
|
|
411
|
-
async def run_js_http_process(
|
|
412
|
+
async def run_js_http_process(
|
|
413
|
+
paths_str: str | None, http_config: dict, watch: bool = False
|
|
414
|
+
):
|
|
412
415
|
# check if tsx is available
|
|
413
416
|
tsx_path = shutil.which("tsx")
|
|
414
417
|
if tsx_path is None:
|
|
@@ -417,7 +420,7 @@ async def run_js_http_process(paths_str: str, http_config: dict, watch: bool = F
|
|
|
417
420
|
)
|
|
418
421
|
|
|
419
422
|
attempt = 0
|
|
420
|
-
while
|
|
423
|
+
while (current_task := asyncio.current_task()) and not current_task.cancelled():
|
|
421
424
|
client_file = os.path.join(os.path.dirname(__file__), "client.http.mts")
|
|
422
425
|
args = ("tsx", "watch", client_file) if watch else ("tsx", client_file)
|
|
423
426
|
pid = None
|
|
@@ -795,7 +798,9 @@ async def wait_until_js_ready():
|
|
|
795
798
|
) as checkpointer_client,
|
|
796
799
|
):
|
|
797
800
|
attempt = 0
|
|
798
|
-
while
|
|
801
|
+
while (
|
|
802
|
+
current_task := asyncio.current_task()
|
|
803
|
+
) and not current_task.cancelled():
|
|
799
804
|
try:
|
|
800
805
|
res = await graph_client.get("/ok")
|
|
801
806
|
res.raise_for_status()
|
|
@@ -909,7 +914,7 @@ class CustomJsAuthBackend(AuthenticationBackend):
|
|
|
909
914
|
|
|
910
915
|
|
|
911
916
|
async def handle_js_auth_event(
|
|
912
|
-
ctx: Auth.types.AuthContext
|
|
917
|
+
ctx: Auth.types.AuthContext,
|
|
913
918
|
value: dict,
|
|
914
919
|
) -> Auth.types.FilterType | None:
|
|
915
920
|
if hasattr(ctx.user, "dict") and callable(ctx.user.dict):
|
langgraph_api/js/src/preload.mjs
CHANGED
|
@@ -11,7 +11,15 @@ const cwd = process.cwd();
|
|
|
11
11
|
// be working fine as well.
|
|
12
12
|
const firstGraphFile =
|
|
13
13
|
Object.values(graphs)
|
|
14
|
-
.
|
|
14
|
+
.map((i) => {
|
|
15
|
+
if (typeof i === "string") {
|
|
16
|
+
return i.split(":").at(0);
|
|
17
|
+
} else if (i && typeof i === "object" && i.path) {
|
|
18
|
+
return i.path.split(":").at(0);
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
})
|
|
22
|
+
.filter(Boolean)
|
|
15
23
|
.at(0) || "index.mts";
|
|
16
24
|
|
|
17
25
|
// enforce API @langchain/langgraph resolution
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export function filterValidExportPath(
|
|
1
|
+
export function filterValidExportPath(
|
|
2
|
+
path: string | { path: string } | undefined,
|
|
3
|
+
) {
|
|
2
4
|
if (!path) return false;
|
|
3
|
-
|
|
5
|
+
const p = typeof path === "string" ? path : path.path;
|
|
6
|
+
return !p.split(":")[0].endsWith(".py");
|
|
4
7
|
}
|
langgraph_api/js/sse.py
CHANGED
|
@@ -91,7 +91,7 @@ class SSEDecoder:
|
|
|
91
91
|
|
|
92
92
|
sse = StreamPart(
|
|
93
93
|
event=self._event,
|
|
94
|
-
data=orjson.loads(self._data) if self._data else None,
|
|
94
|
+
data=orjson.loads(self._data) if self._data else None, # type: ignore[invalid-argument-type]
|
|
95
95
|
)
|
|
96
96
|
|
|
97
97
|
# NOTE: as per the SSE spec, do not reset last_event_id.
|
langgraph_api/logging.py
CHANGED
|
@@ -19,8 +19,8 @@ LOG_LEVEL = log_env("LOG_LEVEL", cast=str, default="INFO")
|
|
|
19
19
|
logging.getLogger().setLevel(LOG_LEVEL.upper())
|
|
20
20
|
logging.getLogger("psycopg").setLevel(logging.WARNING)
|
|
21
21
|
|
|
22
|
-
worker_config
|
|
23
|
-
|
|
22
|
+
worker_config = contextvars.ContextVar[dict[str, typing.Any] | None](
|
|
23
|
+
"worker_config", default=None
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
# custom processors
|
|
@@ -165,6 +165,6 @@ if not structlog.is_configured():
|
|
|
165
165
|
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
166
166
|
],
|
|
167
167
|
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
168
|
-
wrapper_class=structlog.stdlib.BoundLogger,
|
|
168
|
+
wrapper_class=structlog.stdlib.BoundLogger, # type: ignore[invalid-argument-type]
|
|
169
169
|
cache_logger_on_first_use=True,
|
|
170
170
|
)
|
|
@@ -84,6 +84,7 @@ class AccessLoggerMiddleware:
|
|
|
84
84
|
|
|
85
85
|
if method and route and status:
|
|
86
86
|
HTTP_METRICS_COLLECTOR.record_request(method, route, status, latency)
|
|
87
|
+
qs = scope.get("query_string")
|
|
87
88
|
self.logger.log(
|
|
88
89
|
_get_level(status),
|
|
89
90
|
f"{method} {path} {status} {latency}ms",
|
|
@@ -93,7 +94,7 @@ class AccessLoggerMiddleware:
|
|
|
93
94
|
latency_ms=latency,
|
|
94
95
|
route=route,
|
|
95
96
|
path_params=scope.get("path_params"),
|
|
96
|
-
query_string=
|
|
97
|
+
query_string=qs.decode() if qs else "",
|
|
97
98
|
proto=scope.get("http_version"),
|
|
98
99
|
req_header=_headers_to_dict(scope.get("headers")),
|
|
99
100
|
res_header=_headers_to_dict(info["response"].get("headers")),
|
langgraph_api/models/run.py
CHANGED
|
@@ -3,11 +3,11 @@ import time
|
|
|
3
3
|
import urllib.parse
|
|
4
4
|
import uuid
|
|
5
5
|
from collections.abc import Mapping, Sequence
|
|
6
|
-
from typing import Any, NamedTuple
|
|
6
|
+
from typing import Any, NamedTuple, cast
|
|
7
7
|
from uuid import UUID
|
|
8
8
|
|
|
9
9
|
import orjson
|
|
10
|
-
|
|
10
|
+
import structlog
|
|
11
11
|
from starlette.authentication import BaseUser
|
|
12
12
|
from starlette.exceptions import HTTPException
|
|
13
13
|
from typing_extensions import TypedDict
|
|
@@ -27,7 +27,10 @@ from langgraph_api.schema import (
|
|
|
27
27
|
)
|
|
28
28
|
from langgraph_api.utils import AsyncConnectionProto, get_auth_ctx
|
|
29
29
|
from langgraph_api.utils.headers import should_include_header
|
|
30
|
-
from
|
|
30
|
+
from langgraph_api.utils.uuids import uuid7
|
|
31
|
+
from langgraph_runtime.ops import Runs
|
|
32
|
+
|
|
33
|
+
logger = structlog.stdlib.get_logger(__name__)
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
class LangSmithTracer(TypedDict, total=False):
|
|
@@ -183,7 +186,7 @@ LANGSMITH_PROJECT = "langsmith-project"
|
|
|
183
186
|
DEFAULT_RUN_HEADERS_EXCLUDE = {"x-api-key", "x-tenant-id", "x-service-key"}
|
|
184
187
|
|
|
185
188
|
|
|
186
|
-
def get_configurable_headers(headers:
|
|
189
|
+
def get_configurable_headers(headers: Mapping[str, str]) -> dict[str, str]:
|
|
187
190
|
"""Extract headers that should be added to run configuration.
|
|
188
191
|
|
|
189
192
|
This function handles special cases like langsmith-trace and baggage headers,
|
|
@@ -249,7 +252,7 @@ async def create_valid_run(
|
|
|
249
252
|
request_id = headers.get("x-request-id") # Will be null in the crons scheduler.
|
|
250
253
|
(
|
|
251
254
|
assistant_id,
|
|
252
|
-
|
|
255
|
+
thread_id_,
|
|
253
256
|
checkpoint_id,
|
|
254
257
|
run_id,
|
|
255
258
|
) = _get_ids(
|
|
@@ -258,7 +261,7 @@ async def create_valid_run(
|
|
|
258
261
|
run_id=run_id,
|
|
259
262
|
)
|
|
260
263
|
if (
|
|
261
|
-
|
|
264
|
+
thread_id_ is None
|
|
262
265
|
and (command := payload.get("command"))
|
|
263
266
|
and command.get("resume")
|
|
264
267
|
):
|
|
@@ -266,7 +269,9 @@ async def create_valid_run(
|
|
|
266
269
|
status_code=400,
|
|
267
270
|
detail="You must provide a thread_id when resuming.",
|
|
268
271
|
)
|
|
269
|
-
temporary =
|
|
272
|
+
temporary = (
|
|
273
|
+
thread_id_ is None and payload.get("on_completion", "delete") == "delete"
|
|
274
|
+
)
|
|
270
275
|
stream_resumable = payload.get("stream_resumable", False)
|
|
271
276
|
stream_mode, multitask_strategy, prevent_insert_if_inflight = assign_defaults(
|
|
272
277
|
payload
|
|
@@ -296,7 +301,7 @@ async def create_valid_run(
|
|
|
296
301
|
configurable.update(get_configurable_headers(headers))
|
|
297
302
|
ctx = get_auth_ctx()
|
|
298
303
|
if ctx:
|
|
299
|
-
user = ctx.user
|
|
304
|
+
user = cast(BaseUser | None, ctx.user)
|
|
300
305
|
user_id = get_user_id(user)
|
|
301
306
|
configurable["langgraph_auth_user"] = user
|
|
302
307
|
configurable["langgraph_auth_user_id"] = user_id
|
|
@@ -336,7 +341,7 @@ async def create_valid_run(
|
|
|
336
341
|
metadata=payload.get("metadata"),
|
|
337
342
|
status="pending",
|
|
338
343
|
user_id=user_id,
|
|
339
|
-
thread_id=
|
|
344
|
+
thread_id=thread_id_,
|
|
340
345
|
run_id=run_id,
|
|
341
346
|
multitask_strategy=multitask_strategy,
|
|
342
347
|
prevent_insert_if_inflight=prevent_insert_if_inflight,
|
|
@@ -362,7 +367,7 @@ async def create_valid_run(
|
|
|
362
367
|
logger.info(
|
|
363
368
|
"Created run",
|
|
364
369
|
run_id=str(run_id),
|
|
365
|
-
thread_id=str(
|
|
370
|
+
thread_id=str(thread_id_),
|
|
366
371
|
assistant_id=str(assistant_id),
|
|
367
372
|
multitask_strategy=multitask_strategy,
|
|
368
373
|
stream_mode=stream_mode,
|
|
@@ -383,7 +388,7 @@ async def create_valid_run(
|
|
|
383
388
|
await Runs.cancel(
|
|
384
389
|
conn,
|
|
385
390
|
[run["run_id"] for run in inflight_runs],
|
|
386
|
-
thread_id=
|
|
391
|
+
thread_id=thread_id_,
|
|
387
392
|
action=multitask_strategy,
|
|
388
393
|
)
|
|
389
394
|
except HTTPException:
|
|
@@ -415,15 +420,15 @@ def _get_ids(
|
|
|
415
420
|
assistant_id = get_assistant_id(payload["assistant_id"])
|
|
416
421
|
|
|
417
422
|
# ensure UUID validity defaults
|
|
418
|
-
assistant_id,
|
|
423
|
+
assistant_id, thread_id_, checkpoint_id = ensure_ids(
|
|
419
424
|
assistant_id, thread_id, payload
|
|
420
425
|
)
|
|
421
426
|
|
|
422
|
-
run_id = run_id or
|
|
427
|
+
run_id = run_id or uuid7()
|
|
423
428
|
|
|
424
429
|
return _Ids(
|
|
425
430
|
assistant_id,
|
|
426
|
-
|
|
431
|
+
thread_id_,
|
|
427
432
|
checkpoint_id,
|
|
428
433
|
run_id,
|
|
429
434
|
)
|
langgraph_api/patch.py
CHANGED
|
@@ -41,8 +41,8 @@ async def StreamingResponse_stream_response(self, send: Send) -> None:
|
|
|
41
41
|
|
|
42
42
|
# patch StreamingResponse.stream_response
|
|
43
43
|
|
|
44
|
-
StreamingResponse.stream_response = StreamingResponse_stream_response
|
|
44
|
+
StreamingResponse.stream_response = StreamingResponse_stream_response # type: ignore[invalid-assignment]
|
|
45
45
|
|
|
46
46
|
# patch Response.render
|
|
47
47
|
|
|
48
|
-
Response.render = Response_render
|
|
48
|
+
Response.render = Response_render # type: ignore[invalid-assignment]
|
|
@@ -18,9 +18,9 @@ import os
|
|
|
18
18
|
import pathlib
|
|
19
19
|
import signal
|
|
20
20
|
from contextlib import asynccontextmanager
|
|
21
|
+
from typing import cast
|
|
21
22
|
|
|
22
23
|
import structlog
|
|
23
|
-
import uvloop
|
|
24
24
|
|
|
25
25
|
from langgraph_runtime.lifespan import lifespan
|
|
26
26
|
from langgraph_runtime.metrics import get_metrics
|
|
@@ -36,21 +36,22 @@ async def health_and_metrics_server():
|
|
|
36
36
|
class HealthAndMetricsHandler(http.server.SimpleHTTPRequestHandler):
|
|
37
37
|
def log_message(self, format, *args):
|
|
38
38
|
# Skip logging for /ok and /metrics endpoints
|
|
39
|
-
if self
|
|
39
|
+
if getattr(self, "path", None) in ["/ok", "/metrics"]:
|
|
40
40
|
return
|
|
41
41
|
# Log other requests normally
|
|
42
42
|
super().log_message(format, *args)
|
|
43
43
|
|
|
44
44
|
def do_GET(self):
|
|
45
|
-
|
|
45
|
+
path = getattr(self, "path", None)
|
|
46
|
+
if path == "/ok":
|
|
46
47
|
self.send_response(200)
|
|
47
48
|
self.send_header("Content-Type", "application/json")
|
|
48
49
|
self.send_header("Content-Length", ok_len)
|
|
49
50
|
self.end_headers()
|
|
50
51
|
self.wfile.write(ok)
|
|
51
|
-
elif
|
|
52
|
+
elif path == "/metrics":
|
|
52
53
|
metrics = get_metrics()
|
|
53
|
-
worker_metrics = metrics["workers"]
|
|
54
|
+
worker_metrics = cast(dict[str, int], metrics["workers"])
|
|
54
55
|
workers_max = worker_metrics["max"]
|
|
55
56
|
workers_active = worker_metrics["active"]
|
|
56
57
|
workers_available = worker_metrics["available"]
|
|
@@ -93,20 +94,27 @@ async def health_and_metrics_server():
|
|
|
93
94
|
httpd.shutdown()
|
|
94
95
|
|
|
95
96
|
|
|
96
|
-
async def entrypoint(
|
|
97
|
+
async def entrypoint(
|
|
98
|
+
grpc_port: int | None = None, entrypoint_name: str = "python-queue"
|
|
99
|
+
):
|
|
97
100
|
from langgraph_api import logging as lg_logging
|
|
98
101
|
from langgraph_api.api import user_router
|
|
99
102
|
|
|
100
|
-
lg_logging.set_logging_context({"entrypoint":
|
|
103
|
+
lg_logging.set_logging_context({"entrypoint": entrypoint_name})
|
|
101
104
|
tasks: set[asyncio.Task] = set()
|
|
102
105
|
tasks.add(asyncio.create_task(health_and_metrics_server()))
|
|
103
106
|
|
|
104
107
|
original_lifespan = user_router.router.lifespan_context if user_router else None
|
|
105
108
|
|
|
106
109
|
@asynccontextmanager
|
|
107
|
-
async def combined_lifespan(
|
|
110
|
+
async def combined_lifespan(
|
|
111
|
+
app, with_cron_scheduler=False, grpc_port=None, taskset=None
|
|
112
|
+
):
|
|
108
113
|
async with lifespan(
|
|
109
|
-
app,
|
|
114
|
+
app,
|
|
115
|
+
with_cron_scheduler=with_cron_scheduler,
|
|
116
|
+
grpc_port=grpc_port,
|
|
117
|
+
taskset=taskset,
|
|
110
118
|
):
|
|
111
119
|
if original_lifespan:
|
|
112
120
|
async with original_lifespan(app):
|
|
@@ -114,11 +122,13 @@ async def entrypoint():
|
|
|
114
122
|
else:
|
|
115
123
|
yield
|
|
116
124
|
|
|
117
|
-
async with combined_lifespan(
|
|
125
|
+
async with combined_lifespan(
|
|
126
|
+
None, with_cron_scheduler=False, grpc_port=grpc_port, taskset=tasks
|
|
127
|
+
):
|
|
118
128
|
await asyncio.gather(*tasks)
|
|
119
129
|
|
|
120
130
|
|
|
121
|
-
async def main():
|
|
131
|
+
async def main(grpc_port: int | None = None, entrypoint_name: str = "python-queue"):
|
|
122
132
|
"""Run the queue entrypoint and shut down gracefully on SIGTERM/SIGINT."""
|
|
123
133
|
loop = asyncio.get_running_loop()
|
|
124
134
|
stop_event = asyncio.Event()
|
|
@@ -132,11 +142,9 @@ async def main():
|
|
|
132
142
|
except (NotImplementedError, RuntimeError):
|
|
133
143
|
signal.signal(signal.SIGTERM, lambda *_: _handle_signal())
|
|
134
144
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
entry_task = asyncio.create_task(entrypoint())
|
|
145
|
+
entry_task = asyncio.create_task(
|
|
146
|
+
entrypoint(grpc_port=grpc_port, entrypoint_name=entrypoint_name)
|
|
147
|
+
)
|
|
140
148
|
await stop_event.wait()
|
|
141
149
|
|
|
142
150
|
logger.warning("Cancelling queue entrypoint task")
|
|
@@ -146,10 +154,17 @@ async def main():
|
|
|
146
154
|
|
|
147
155
|
|
|
148
156
|
if __name__ == "__main__":
|
|
157
|
+
from langgraph_api import config
|
|
158
|
+
|
|
159
|
+
config.IS_QUEUE_ENTRYPOINT = True
|
|
149
160
|
with open(pathlib.Path(__file__).parent.parent / "logging.json") as file:
|
|
150
161
|
loaded_config = json.load(file)
|
|
151
162
|
logging.config.dictConfig(loaded_config)
|
|
163
|
+
try:
|
|
164
|
+
import uvloop # type: ignore[unresolved-import]
|
|
152
165
|
|
|
153
|
-
|
|
154
|
-
|
|
166
|
+
uvloop.install()
|
|
167
|
+
except ImportError:
|
|
168
|
+
pass
|
|
169
|
+
# run the entrypoint
|
|
155
170
|
asyncio.run(main())
|
langgraph_api/schema.py
CHANGED
|
@@ -3,6 +3,7 @@ from datetime import datetime
|
|
|
3
3
|
from typing import Any, Literal, Optional, TypeAlias
|
|
4
4
|
from uuid import UUID
|
|
5
5
|
|
|
6
|
+
from langchain_core.runnables.config import RunnableConfig
|
|
6
7
|
from typing_extensions import TypedDict
|
|
7
8
|
|
|
8
9
|
from langgraph_api.serde import Fragment
|
|
@@ -157,6 +158,22 @@ class ThreadState(TypedDict):
|
|
|
157
158
|
"""The interrupts for this state."""
|
|
158
159
|
|
|
159
160
|
|
|
161
|
+
class RunKwargs(TypedDict):
|
|
162
|
+
config: RunnableConfig
|
|
163
|
+
context: dict[str, Any]
|
|
164
|
+
input: dict[str, Any] | None
|
|
165
|
+
command: dict[str, Any] | None
|
|
166
|
+
stream_mode: StreamMode
|
|
167
|
+
interrupt_before: Sequence[str] | str | None
|
|
168
|
+
interrupt_after: Sequence[str] | str | None
|
|
169
|
+
webhook: str | None
|
|
170
|
+
feedback_keys: Sequence[str] | None
|
|
171
|
+
temporary: bool
|
|
172
|
+
subgraphs: bool
|
|
173
|
+
resumable: bool
|
|
174
|
+
checkpoint_during: bool
|
|
175
|
+
|
|
176
|
+
|
|
160
177
|
class Run(TypedDict):
|
|
161
178
|
run_id: UUID
|
|
162
179
|
"""The ID of the run."""
|
|
@@ -172,7 +189,7 @@ class Run(TypedDict):
|
|
|
172
189
|
"""The status of the run. One of 'pending', 'error', 'success'."""
|
|
173
190
|
metadata: Fragment
|
|
174
191
|
"""The run metadata."""
|
|
175
|
-
kwargs:
|
|
192
|
+
kwargs: RunKwargs
|
|
176
193
|
"""The run kwargs."""
|
|
177
194
|
multitask_strategy: MultitaskStrategy
|
|
178
195
|
"""Strategy to handle concurrent runs on the same thread."""
|
|
@@ -214,6 +231,8 @@ class Cron(TypedDict):
|
|
|
214
231
|
"""The next run date of the cron."""
|
|
215
232
|
metadata: Fragment
|
|
216
233
|
"""The cron metadata."""
|
|
234
|
+
now: datetime
|
|
235
|
+
"""The current time."""
|
|
217
236
|
|
|
218
237
|
|
|
219
238
|
class ThreadUpdateResponse(TypedDict):
|
langgraph_api/serde.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import base64
|
|
2
3
|
import re
|
|
3
4
|
import uuid
|
|
4
5
|
from base64 import b64encode
|
|
@@ -16,7 +17,7 @@ from ipaddress import (
|
|
|
16
17
|
)
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from re import Pattern
|
|
19
|
-
from typing import Any, NamedTuple
|
|
20
|
+
from typing import Any, NamedTuple, cast
|
|
20
21
|
from zoneinfo import ZoneInfo
|
|
21
22
|
|
|
22
23
|
import cloudpickle
|
|
@@ -46,10 +47,14 @@ def decimal_encoder(dec_value: Decimal) -> int | float:
|
|
|
46
47
|
>>> decimal_encoder(Decimal("1"))
|
|
47
48
|
1
|
|
48
49
|
"""
|
|
49
|
-
if
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
if (
|
|
51
|
+
# maps to float('nan') / float('inf') / float('-inf')
|
|
52
|
+
not dec_value.is_finite()
|
|
53
|
+
# or regular float
|
|
54
|
+
or cast(int, dec_value.as_tuple().exponent) < 0
|
|
55
|
+
):
|
|
52
56
|
return float(dec_value)
|
|
57
|
+
return int(dec_value)
|
|
53
58
|
|
|
54
59
|
|
|
55
60
|
def default(obj):
|
|
@@ -142,7 +147,7 @@ def json_loads(content: bytes | Fragment | dict) -> Any:
|
|
|
142
147
|
content = content.buf
|
|
143
148
|
if isinstance(content, dict):
|
|
144
149
|
return content
|
|
145
|
-
return orjson.loads(content)
|
|
150
|
+
return orjson.loads(cast(bytes, content))
|
|
146
151
|
|
|
147
152
|
|
|
148
153
|
async def ajson_loads(content: bytes | Fragment) -> Any:
|
|
@@ -170,3 +175,25 @@ class Serializer(JsonPlusSerializer):
|
|
|
170
175
|
)
|
|
171
176
|
return None
|
|
172
177
|
return super().loads_typed(data)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
mpack_keys = {"method", "value"}
|
|
181
|
+
SERIALIZER = Serializer()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# TODO: Make more performant (by removing)
|
|
185
|
+
async def reserialize_message(message: bytes) -> bytes:
|
|
186
|
+
# Stream messages from golang runtime are a byte dict of StreamChunks.
|
|
187
|
+
loaded = await ajson_loads(message)
|
|
188
|
+
converted = {}
|
|
189
|
+
for k, v in loaded.items():
|
|
190
|
+
if isinstance(v, dict) and v.keys() == mpack_keys:
|
|
191
|
+
if v["method"] == "missing":
|
|
192
|
+
converted[k] = v["value"] # oops
|
|
193
|
+
else:
|
|
194
|
+
converted[k] = SERIALIZER.loads_typed(
|
|
195
|
+
(v["method"], base64.b64decode(v["value"]))
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
converted[k] = v
|
|
199
|
+
return json_dumpb(converted)
|
langgraph_api/server.py
CHANGED
|
@@ -122,17 +122,19 @@ if user_router:
|
|
|
122
122
|
# Merge routes
|
|
123
123
|
app = user_router
|
|
124
124
|
|
|
125
|
-
meta_route_paths = [
|
|
125
|
+
meta_route_paths = [
|
|
126
|
+
getattr(route, "path", None) for route in meta_routes if hasattr(route, "path")
|
|
127
|
+
]
|
|
126
128
|
custom_route_paths = [
|
|
127
129
|
route.path
|
|
128
130
|
for route in user_router.router.routes
|
|
129
|
-
if route.path not in meta_route_paths
|
|
131
|
+
if hasattr(route, "path") and route.path not in meta_route_paths
|
|
130
132
|
]
|
|
131
133
|
logger.info(f"Custom route paths: {custom_route_paths}")
|
|
132
134
|
|
|
133
135
|
update_openapi_spec(app)
|
|
134
136
|
for route in routes:
|
|
135
|
-
if route
|
|
137
|
+
if getattr(route, "path", None) in ("/docs", "/openapi.json"):
|
|
136
138
|
# Our handlers for these are inclusive of the custom routes and default API ones
|
|
137
139
|
# Don't let these be shadowed
|
|
138
140
|
app.router.routes.insert(0, route)
|
langgraph_api/state.py
CHANGED
|
@@ -60,11 +60,11 @@ def patch_interrupt(
|
|
|
60
60
|
return {"id": id, **interrupt.raw}
|
|
61
61
|
|
|
62
62
|
if USE_NEW_INTERRUPTS:
|
|
63
|
-
interrupt = Interrupt(**interrupt) if isinstance(interrupt, dict) else interrupt
|
|
63
|
+
interrupt = Interrupt(**interrupt) if isinstance(interrupt, dict) else interrupt # type: ignore[missing-argument]
|
|
64
64
|
|
|
65
65
|
return {
|
|
66
|
-
"id": interrupt.id,
|
|
67
|
-
"value": interrupt.value,
|
|
66
|
+
"id": interrupt.id, # type: ignore[unresolved-attribute]
|
|
67
|
+
"value": interrupt.value, # type: ignore[unresolved-attribute]
|
|
68
68
|
}
|
|
69
69
|
else:
|
|
70
70
|
if isinstance(interrupt, dict):
|
|
@@ -72,16 +72,16 @@ def patch_interrupt(
|
|
|
72
72
|
# id is the new field we use for identification, also not supported on init for old versions
|
|
73
73
|
interrupt.pop("interrupt_id", None)
|
|
74
74
|
interrupt.pop("id", None)
|
|
75
|
-
interrupt = Interrupt(**interrupt)
|
|
75
|
+
interrupt = Interrupt(**interrupt) # type: ignore[missing-argument]
|
|
76
76
|
|
|
77
77
|
return {
|
|
78
78
|
"id": interrupt.interrupt_id
|
|
79
79
|
if hasattr(interrupt, "interrupt_id")
|
|
80
80
|
else None,
|
|
81
|
-
"value": interrupt.value,
|
|
82
|
-
"resumable": interrupt.resumable,
|
|
83
|
-
"ns": interrupt.ns,
|
|
84
|
-
"when": interrupt.when,
|
|
81
|
+
"value": interrupt.value, # type: ignore[unresolved-attribute]
|
|
82
|
+
"resumable": interrupt.resumable, # type: ignore[unresolved-attribute]
|
|
83
|
+
"ns": interrupt.ns, # type: ignore[unresolved-attribute]
|
|
84
|
+
"when": interrupt.when, # type: ignore[unresolved-attribute]
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
|
langgraph_api/store.py
CHANGED
|
@@ -93,7 +93,7 @@ def _load_store(store_path: str) -> Any:
|
|
|
93
93
|
raise ValueError(f"Could not find store file: {path_name}")
|
|
94
94
|
module = importlib.util.module_from_spec(modspec)
|
|
95
95
|
sys.modules[module_name] = module
|
|
96
|
-
modspec.loader.exec_module(module)
|
|
96
|
+
modspec.loader.exec_module(module) # type: ignore[possibly-unbound-attribute]
|
|
97
97
|
|
|
98
98
|
else:
|
|
99
99
|
path_name, function = store_path.rsplit(".", 1)
|