langgraph-api 0.3.4__tar.gz → 0.4.0__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.3.4 → langgraph_api-0.4.0}/PKG-INFO +1 -1
- langgraph_api-0.4.0/langgraph_api/__init__.py +1 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/api/runs.py +13 -8
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/api/threads.py +24 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/models/run.py +5 -4
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/stream.py +6 -2
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/utils/__init__.py +19 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/worker.py +4 -2
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/openapi.json +67 -0
- langgraph_api-0.3.4/langgraph_api/__init__.py +0 -1
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/.gitignore +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/LICENSE +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/Makefile +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/README.md +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/benchmark/.gitignore +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/benchmark/Makefile +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/benchmark/README.md +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/benchmark/burst.js +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/benchmark/clean.js +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/benchmark/graphs.js +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/benchmark/package.json +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/benchmark/ramp.js +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/benchmark/update-revision.js +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/benchmark/weather.js +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/constraints.txt +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/forbidden.txt +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/healthcheck.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/api/__init__.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/api/assistants.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/api/mcp.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/api/meta.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/api/openapi.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/api/store.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/api/ui.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/asgi_transport.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/asyncio.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/auth/__init__.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/auth/custom.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/auth/langsmith/__init__.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/auth/langsmith/backend.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/auth/langsmith/client.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/auth/middleware.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/auth/noop.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/auth/studio_user.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/cli.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/command.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/config.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/cron_scheduler.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/errors.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/executor_entrypoint.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/feature_flags.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/graph.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/http.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/http_metrics.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/.gitignore +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/.prettierrc +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/__init__.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/base.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/build.mts +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/client.http.mts +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/client.mts +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/errors.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/global.d.ts +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/package.json +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/remote.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/schema.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/src/graph.mts +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/src/load.hooks.mjs +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/src/preload.mjs +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/src/utils/files.mts +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/src/utils/importMap.mts +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/src/utils/serde.mts +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/sse.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/traceblock.mts +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/tsconfig.json +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/ui.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/js/yarn.lock +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/logging.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/metadata.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/middleware/__init__.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/middleware/http_logger.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/middleware/private_network.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/middleware/request_id.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/models/__init__.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/patch.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/queue_entrypoint.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/route.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/schema.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/serde.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/server.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/sse.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/state.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/store.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/thread_ttl.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/traceblock.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/tunneling/cloudflare.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/utils/cache.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/utils/config.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/utils/future.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/utils/headers.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/utils/uuids.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/utils.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/validation.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_api/webhook.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_license/__init__.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_license/validation.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_runtime/__init__.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_runtime/checkpoint.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_runtime/database.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_runtime/lifespan.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_runtime/metrics.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_runtime/ops.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_runtime/queue.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_runtime/retry.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/langgraph_runtime/store.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/logging.json +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/pyproject.toml +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/scripts/create_license.py +0 -0
- {langgraph_api-0.3.4 → langgraph_api-0.4.0}/uv.lock +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from collections.abc import AsyncIterator
|
|
3
3
|
from typing import Literal, cast
|
|
4
|
+
from uuid import uuid4
|
|
4
5
|
|
|
5
6
|
import orjson
|
|
6
7
|
from starlette.exceptions import HTTPException
|
|
@@ -100,7 +101,7 @@ async def stream_run(
|
|
|
100
101
|
payload = await request.json(RunCreateStateful)
|
|
101
102
|
on_disconnect = payload.get("on_disconnect", "continue")
|
|
102
103
|
run_id = uuid7()
|
|
103
|
-
sub = asyncio.create_task(Runs.Stream.subscribe(run_id))
|
|
104
|
+
sub = asyncio.create_task(Runs.Stream.subscribe(run_id, thread_id))
|
|
104
105
|
|
|
105
106
|
try:
|
|
106
107
|
async with connect() as conn:
|
|
@@ -138,19 +139,21 @@ async def stream_run_stateless(
|
|
|
138
139
|
):
|
|
139
140
|
"""Create a stateless run."""
|
|
140
141
|
payload = await request.json(RunCreateStateless)
|
|
142
|
+
payload["if_not_exists"] = "create"
|
|
141
143
|
on_disconnect = payload.get("on_disconnect", "continue")
|
|
142
144
|
run_id = uuid7()
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
thread_id = uuid4()
|
|
146
|
+
sub = asyncio.create_task(Runs.Stream.subscribe(run_id, thread_id))
|
|
145
147
|
try:
|
|
146
148
|
async with connect() as conn:
|
|
147
149
|
run = await create_valid_run(
|
|
148
150
|
conn,
|
|
149
|
-
|
|
151
|
+
str(thread_id),
|
|
150
152
|
payload,
|
|
151
153
|
request.headers,
|
|
152
154
|
run_id=run_id,
|
|
153
155
|
request_start_time=request.scope.get("request_start_time_ms"),
|
|
156
|
+
temporary=True,
|
|
154
157
|
)
|
|
155
158
|
except Exception:
|
|
156
159
|
if not sub.cancelled():
|
|
@@ -181,7 +184,7 @@ async def wait_run(request: ApiRequest):
|
|
|
181
184
|
payload = await request.json(RunCreateStateful)
|
|
182
185
|
on_disconnect = payload.get("on_disconnect", "continue")
|
|
183
186
|
run_id = uuid7()
|
|
184
|
-
sub = asyncio.create_task(Runs.Stream.subscribe(run_id))
|
|
187
|
+
sub = asyncio.create_task(Runs.Stream.subscribe(run_id, thread_id))
|
|
185
188
|
|
|
186
189
|
try:
|
|
187
190
|
async with connect() as conn:
|
|
@@ -263,26 +266,28 @@ async def wait_run(request: ApiRequest):
|
|
|
263
266
|
async def wait_run_stateless(request: ApiRequest):
|
|
264
267
|
"""Create a stateless run, wait for the output."""
|
|
265
268
|
payload = await request.json(RunCreateStateless)
|
|
269
|
+
payload["if_not_exists"] = "create"
|
|
266
270
|
on_disconnect = payload.get("on_disconnect", "continue")
|
|
267
271
|
run_id = uuid7()
|
|
268
|
-
|
|
272
|
+
thread_id = uuid4()
|
|
273
|
+
sub = asyncio.create_task(Runs.Stream.subscribe(run_id, thread_id))
|
|
269
274
|
|
|
270
275
|
try:
|
|
271
276
|
async with connect() as conn:
|
|
272
277
|
run = await create_valid_run(
|
|
273
278
|
conn,
|
|
274
|
-
|
|
279
|
+
str(thread_id),
|
|
275
280
|
payload,
|
|
276
281
|
request.headers,
|
|
277
282
|
run_id=run_id,
|
|
278
283
|
request_start_time=request.scope.get("request_start_time_ms"),
|
|
284
|
+
temporary=True,
|
|
279
285
|
)
|
|
280
286
|
except Exception:
|
|
281
287
|
if not sub.cancelled():
|
|
282
288
|
handle = await sub
|
|
283
289
|
await handle.__aexit__(None, None, None)
|
|
284
290
|
raise
|
|
285
|
-
|
|
286
291
|
last_chunk = ValueEvent()
|
|
287
292
|
|
|
288
293
|
async def consume():
|
|
@@ -6,11 +6,13 @@ from starlette.routing import BaseRoute
|
|
|
6
6
|
|
|
7
7
|
from langgraph_api.route import ApiRequest, ApiResponse, ApiRoute
|
|
8
8
|
from langgraph_api.schema import THREAD_FIELDS
|
|
9
|
+
from langgraph_api.sse import EventSourceResponse
|
|
9
10
|
from langgraph_api.state import state_snapshot_to_thread_state
|
|
10
11
|
from langgraph_api.utils import (
|
|
11
12
|
fetchone,
|
|
12
13
|
get_pagination_headers,
|
|
13
14
|
validate_select_columns,
|
|
15
|
+
validate_stream_id,
|
|
14
16
|
validate_uuid,
|
|
15
17
|
)
|
|
16
18
|
from langgraph_api.validation import (
|
|
@@ -282,6 +284,23 @@ async def copy_thread(request: ApiRequest):
|
|
|
282
284
|
return ApiResponse(await fetchone(iter, not_found_code=409))
|
|
283
285
|
|
|
284
286
|
|
|
287
|
+
@retry_db
|
|
288
|
+
async def join_thread_stream(request: ApiRequest):
|
|
289
|
+
"""Join a thread stream."""
|
|
290
|
+
thread_id = request.path_params["thread_id"]
|
|
291
|
+
validate_uuid(thread_id, "Invalid thread ID: must be a UUID")
|
|
292
|
+
last_event_id = request.headers.get("last-event-id") or None
|
|
293
|
+
validate_stream_id(
|
|
294
|
+
last_event_id, "Invalid last-event-id: must be a valid Redis stream ID"
|
|
295
|
+
)
|
|
296
|
+
return EventSourceResponse(
|
|
297
|
+
Threads.Stream.join(
|
|
298
|
+
thread_id,
|
|
299
|
+
last_event_id=last_event_id,
|
|
300
|
+
),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
285
304
|
threads_routes: list[BaseRoute] = [
|
|
286
305
|
ApiRoute("/threads", endpoint=create_thread, methods=["POST"]),
|
|
287
306
|
ApiRoute("/threads/search", endpoint=search_threads, methods=["POST"]),
|
|
@@ -312,4 +331,9 @@ threads_routes: list[BaseRoute] = [
|
|
|
312
331
|
endpoint=get_thread_state_at_checkpoint_post,
|
|
313
332
|
methods=["POST"],
|
|
314
333
|
),
|
|
334
|
+
ApiRoute(
|
|
335
|
+
"/threads/{thread_id}/stream",
|
|
336
|
+
endpoint=join_thread_stream,
|
|
337
|
+
methods=["GET"],
|
|
338
|
+
),
|
|
315
339
|
]
|
|
@@ -249,6 +249,7 @@ async def create_valid_run(
|
|
|
249
249
|
barrier: asyncio.Barrier | None = None,
|
|
250
250
|
run_id: UUID | None = None,
|
|
251
251
|
request_start_time: float | None = None,
|
|
252
|
+
temporary: bool = False,
|
|
252
253
|
) -> Run:
|
|
253
254
|
request_id = headers.get("x-request-id") # Will be null in the crons scheduler.
|
|
254
255
|
(
|
|
@@ -262,7 +263,7 @@ async def create_valid_run(
|
|
|
262
263
|
run_id=run_id,
|
|
263
264
|
)
|
|
264
265
|
if (
|
|
265
|
-
thread_id_ is None
|
|
266
|
+
(thread_id_ is None or temporary)
|
|
266
267
|
and (command := payload.get("command"))
|
|
267
268
|
and command.get("resume")
|
|
268
269
|
):
|
|
@@ -270,9 +271,9 @@ async def create_valid_run(
|
|
|
270
271
|
status_code=400,
|
|
271
272
|
detail="You must provide a thread_id when resuming.",
|
|
272
273
|
)
|
|
273
|
-
temporary = (
|
|
274
|
-
|
|
275
|
-
)
|
|
274
|
+
temporary = (temporary or thread_id_ is None) and payload.get(
|
|
275
|
+
"on_completion", "delete"
|
|
276
|
+
) == "delete"
|
|
276
277
|
stream_resumable = payload.get("stream_resumable", False)
|
|
277
278
|
stream_mode, multitask_strategy, prevent_insert_if_inflight = assign_defaults(
|
|
278
279
|
payload
|
|
@@ -2,7 +2,7 @@ import uuid
|
|
|
2
2
|
from collections.abc import AsyncIterator, Callable
|
|
3
3
|
from contextlib import AsyncExitStack, aclosing, asynccontextmanager
|
|
4
4
|
from functools import lru_cache
|
|
5
|
-
from typing import Any,
|
|
5
|
+
from typing import Any, cast
|
|
6
6
|
|
|
7
7
|
import langgraph.version
|
|
8
8
|
import langsmith
|
|
@@ -423,7 +423,9 @@ async def consume(
|
|
|
423
423
|
stream: AnyStream,
|
|
424
424
|
run_id: str | uuid.UUID,
|
|
425
425
|
resumable: bool = False,
|
|
426
|
-
stream_modes: set[StreamMode
|
|
426
|
+
stream_modes: set[StreamMode] | None = None,
|
|
427
|
+
*,
|
|
428
|
+
thread_id: str | uuid.UUID | None = None,
|
|
427
429
|
) -> None:
|
|
428
430
|
stream_modes = stream_modes or set()
|
|
429
431
|
if "messages-tuple" in stream_modes:
|
|
@@ -437,6 +439,7 @@ async def consume(
|
|
|
437
439
|
run_id,
|
|
438
440
|
mode,
|
|
439
441
|
await run_in_executor(None, json_dumpb, payload),
|
|
442
|
+
thread_id=thread_id,
|
|
440
443
|
resumable=resumable and mode.split("|")[0] in stream_modes,
|
|
441
444
|
)
|
|
442
445
|
except Exception as e:
|
|
@@ -446,6 +449,7 @@ async def consume(
|
|
|
446
449
|
run_id,
|
|
447
450
|
"error",
|
|
448
451
|
await run_in_executor(None, json_dumpb, e),
|
|
452
|
+
thread_id=thread_id,
|
|
449
453
|
)
|
|
450
454
|
raise e
|
|
451
455
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import contextvars
|
|
2
|
+
import re
|
|
2
3
|
import uuid
|
|
3
4
|
from collections.abc import AsyncIterator
|
|
4
5
|
from contextlib import asynccontextmanager
|
|
@@ -22,6 +23,7 @@ Row: TypeAlias = dict[str, Any]
|
|
|
22
23
|
AuthContext = contextvars.ContextVar[Auth.types.BaseAuthContext | None](
|
|
23
24
|
"AuthContext", default=None
|
|
24
25
|
)
|
|
26
|
+
STREAM_ID_PATTERN = re.compile(r"^\d+(-(\d+|\*))?$")
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
@asynccontextmanager
|
|
@@ -101,6 +103,23 @@ def validate_uuid(uuid_str: str, invalid_uuid_detail: str | None) -> uuid.UUID:
|
|
|
101
103
|
raise HTTPException(status_code=422, detail=invalid_uuid_detail) from None
|
|
102
104
|
|
|
103
105
|
|
|
106
|
+
def validate_stream_id(stream_id: str | None, invalid_stream_id_detail: str | None):
|
|
107
|
+
"""
|
|
108
|
+
Validate Redis stream ID format.
|
|
109
|
+
Valid formats:
|
|
110
|
+
- timestamp-sequence (e.g., "1724342400000-0")
|
|
111
|
+
- timestamp-* (e.g., "1724342400000-*")
|
|
112
|
+
- timestamp only (e.g., "1724342400000")
|
|
113
|
+
- "-" (special case, represents the beginning of the stream, use if you want to replay all events)
|
|
114
|
+
"""
|
|
115
|
+
if not stream_id or stream_id == "-":
|
|
116
|
+
return stream_id
|
|
117
|
+
|
|
118
|
+
if STREAM_ID_PATTERN.match(stream_id):
|
|
119
|
+
return stream_id
|
|
120
|
+
raise HTTPException(status_code=422, detail=invalid_stream_id_detail)
|
|
121
|
+
|
|
122
|
+
|
|
104
123
|
def next_cron_date(schedule: str, base_time: datetime) -> datetime:
|
|
105
124
|
import croniter # type: ignore[unresolved-import]
|
|
106
125
|
|
|
@@ -139,7 +139,9 @@ async def worker(
|
|
|
139
139
|
stream_modes: set[StreamMode],
|
|
140
140
|
):
|
|
141
141
|
try:
|
|
142
|
-
await consume(
|
|
142
|
+
await consume(
|
|
143
|
+
stream, run_id, resumable, stream_modes, thread_id=run["thread_id"]
|
|
144
|
+
)
|
|
143
145
|
except Exception as e:
|
|
144
146
|
if not isinstance(e, UserRollback | UserInterrupt):
|
|
145
147
|
logger.exception(
|
|
@@ -151,7 +153,7 @@ async def worker(
|
|
|
151
153
|
raise UserTimeout(e) from e
|
|
152
154
|
raise
|
|
153
155
|
|
|
154
|
-
async with Runs.enter(run_id, main_loop) as done:
|
|
156
|
+
async with Runs.enter(run_id, run["thread_id"], main_loop) as done:
|
|
155
157
|
# attempt the run
|
|
156
158
|
try:
|
|
157
159
|
if attempt > BG_JOB_MAX_RETRIES:
|
|
@@ -1520,6 +1520,73 @@
|
|
|
1520
1520
|
}
|
|
1521
1521
|
}
|
|
1522
1522
|
},
|
|
1523
|
+
"/threads/{thread_id}/stream": {
|
|
1524
|
+
"get": {
|
|
1525
|
+
"tags": [
|
|
1526
|
+
"Threads"
|
|
1527
|
+
],
|
|
1528
|
+
"summary": "Join Thread Stream",
|
|
1529
|
+
"description": "This endpoint streams output in real-time from a thread. The stream will include the output of each run executed sequentially on the thread and will remain open indefinitely. It is the responsibility of the calling client to close the connection.",
|
|
1530
|
+
"operationId": "join_thread_stream_threads__thread_id__stream_get",
|
|
1531
|
+
"parameters": [
|
|
1532
|
+
{
|
|
1533
|
+
"description": "The ID of the thread.",
|
|
1534
|
+
"required": true,
|
|
1535
|
+
"schema": {
|
|
1536
|
+
"type": "string",
|
|
1537
|
+
"format": "uuid",
|
|
1538
|
+
"title": "Thread Id",
|
|
1539
|
+
"description": "The ID of the thread."
|
|
1540
|
+
},
|
|
1541
|
+
"name": "thread_id",
|
|
1542
|
+
"in": "path"
|
|
1543
|
+
},
|
|
1544
|
+
{
|
|
1545
|
+
"required": false,
|
|
1546
|
+
"schema": {
|
|
1547
|
+
"type": "string",
|
|
1548
|
+
"title": "Last Event ID",
|
|
1549
|
+
"description": "The ID of the last event received. Used to resume streaming from a specific point. Pass '-' to resume from the beginning."
|
|
1550
|
+
},
|
|
1551
|
+
"name": "Last-Event-ID",
|
|
1552
|
+
"in": "header"
|
|
1553
|
+
}
|
|
1554
|
+
],
|
|
1555
|
+
"responses": {
|
|
1556
|
+
"200": {
|
|
1557
|
+
"description": "Success",
|
|
1558
|
+
"content": {
|
|
1559
|
+
"text/event-stream": {
|
|
1560
|
+
"schema": {
|
|
1561
|
+
"type": "string",
|
|
1562
|
+
"description": "The server will send a stream of events in SSE format.\n\n**Example event**:\n\nid: 1\n\nevent: message\n\ndata: {}"
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
},
|
|
1567
|
+
"404": {
|
|
1568
|
+
"description": "Not Found",
|
|
1569
|
+
"content": {
|
|
1570
|
+
"application/json": {
|
|
1571
|
+
"schema": {
|
|
1572
|
+
"$ref": "#/components/schemas/ErrorResponse"
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
},
|
|
1577
|
+
"422": {
|
|
1578
|
+
"description": "Validation Error",
|
|
1579
|
+
"content": {
|
|
1580
|
+
"application/json": {
|
|
1581
|
+
"schema": {
|
|
1582
|
+
"$ref": "#/components/schemas/ErrorResponse"
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
},
|
|
1523
1590
|
"/threads/{thread_id}/runs": {
|
|
1524
1591
|
"get": {
|
|
1525
1592
|
"tags": [
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.4"
|
|
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
|
|
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
|
|
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
|