langgraph-api 0.0.26__py3-none-any.whl → 0.0.28rc1__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/api/__init__.py +2 -0
- langgraph_api/api/assistants.py +43 -13
- langgraph_api/api/meta.py +2 -1
- langgraph_api/api/runs.py +14 -1
- langgraph_api/api/ui.py +68 -0
- langgraph_api/asyncio.py +43 -4
- langgraph_api/auth/middleware.py +2 -2
- langgraph_api/cli.py +72 -57
- langgraph_api/config.py +23 -1
- langgraph_api/cron_scheduler.py +1 -1
- langgraph_api/graph.py +5 -0
- langgraph_api/http.py +24 -7
- langgraph_api/js/.gitignore +2 -0
- langgraph_api/js/build.mts +49 -3
- langgraph_api/js/client.mts +84 -40
- langgraph_api/js/global.d.ts +1 -0
- langgraph_api/js/package.json +15 -7
- langgraph_api/js/remote.py +662 -16
- langgraph_api/js/src/graph.mts +5 -4
- langgraph_api/js/sse.py +138 -0
- langgraph_api/js/tests/api.test.mts +28 -0
- langgraph_api/js/tests/compose-postgres.yml +2 -2
- langgraph_api/js/tests/graphs/agent.css +1 -0
- langgraph_api/js/tests/graphs/agent.ui.tsx +10 -0
- langgraph_api/js/tests/graphs/package.json +2 -2
- langgraph_api/js/tests/graphs/yarn.lock +13 -13
- langgraph_api/js/yarn.lock +710 -1187
- langgraph_api/lifespan.py +15 -5
- langgraph_api/logging.py +9 -0
- langgraph_api/metadata.py +5 -1
- langgraph_api/middleware/http_logger.py +1 -1
- langgraph_api/patch.py +2 -0
- langgraph_api/queue_entrypoint.py +63 -0
- langgraph_api/schema.py +2 -0
- langgraph_api/stream.py +1 -0
- langgraph_api/webhook.py +42 -0
- langgraph_api/{queue.py → worker.py} +52 -166
- {langgraph_api-0.0.26.dist-info → langgraph_api-0.0.28rc1.dist-info}/METADATA +8 -8
- {langgraph_api-0.0.26.dist-info → langgraph_api-0.0.28rc1.dist-info}/RECORD +49 -46
- langgraph_storage/database.py +8 -22
- langgraph_storage/inmem_stream.py +108 -0
- langgraph_storage/ops.py +80 -57
- langgraph_storage/queue.py +126 -103
- langgraph_storage/retry.py +5 -1
- langgraph_storage/store.py +5 -1
- openapi.json +3 -3
- langgraph_api/js/client.new.mts +0 -861
- langgraph_api/js/remote_new.py +0 -694
- langgraph_api/js/remote_old.py +0 -667
- langgraph_api/js/server_sent_events.py +0 -126
- {langgraph_api-0.0.26.dist-info → langgraph_api-0.0.28rc1.dist-info}/LICENSE +0 -0
- {langgraph_api-0.0.26.dist-info → langgraph_api-0.0.28rc1.dist-info}/WHEEL +0 -0
- {langgraph_api-0.0.26.dist-info → langgraph_api-0.0.28rc1.dist-info}/entry_points.txt +0 -0
langgraph_api/lifespan.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
from contextlib import asynccontextmanager
|
|
2
3
|
|
|
3
4
|
from starlette.applications import Starlette
|
|
@@ -8,13 +9,17 @@ from langgraph_api.cron_scheduler import cron_scheduler
|
|
|
8
9
|
from langgraph_api.graph import collect_graphs_from_env, stop_remote_graphs
|
|
9
10
|
from langgraph_api.http import start_http_client, stop_http_client
|
|
10
11
|
from langgraph_api.metadata import metadata_loop
|
|
11
|
-
from langgraph_api.queue import queue
|
|
12
12
|
from langgraph_license.validation import get_license_status, plus_features_enabled
|
|
13
13
|
from langgraph_storage.database import start_pool, stop_pool
|
|
14
|
+
from langgraph_storage.queue import queue
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
@asynccontextmanager
|
|
17
|
-
async def lifespan(
|
|
18
|
+
async def lifespan(
|
|
19
|
+
app: Starlette | None = None,
|
|
20
|
+
with_cron_scheduler: bool = True,
|
|
21
|
+
taskset: set[asyncio.Task] | None = None,
|
|
22
|
+
):
|
|
18
23
|
if not await get_license_status():
|
|
19
24
|
raise ValueError(
|
|
20
25
|
"License verification failed. Please ensure proper configuration:\n"
|
|
@@ -29,10 +34,15 @@ async def lifespan(app: Starlette):
|
|
|
29
34
|
await start_pool()
|
|
30
35
|
await collect_graphs_from_env(True)
|
|
31
36
|
try:
|
|
32
|
-
async with SimpleTaskGroup(cancel=True) as tg:
|
|
37
|
+
async with SimpleTaskGroup(cancel=True, taskset=taskset) as tg:
|
|
33
38
|
tg.create_task(metadata_loop())
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
if config.N_JOBS_PER_WORKER > 0:
|
|
40
|
+
tg.create_task(queue())
|
|
41
|
+
if (
|
|
42
|
+
with_cron_scheduler
|
|
43
|
+
and config.FF_CRONS_ENABLED
|
|
44
|
+
and plus_features_enabled()
|
|
45
|
+
):
|
|
36
46
|
tg.create_task(cron_scheduler())
|
|
37
47
|
yield
|
|
38
48
|
finally:
|
langgraph_api/logging.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
+
import threading
|
|
3
4
|
|
|
4
5
|
import structlog
|
|
5
6
|
from starlette.config import Config
|
|
@@ -22,6 +23,13 @@ logging.getLogger("psycopg").setLevel(logging.WARNING)
|
|
|
22
23
|
# custom processors
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
def add_thread_name(
|
|
27
|
+
logger: logging.Logger, method_name: str, event_dict: EventDict
|
|
28
|
+
) -> EventDict:
|
|
29
|
+
event_dict["thread_name"] = threading.current_thread().name
|
|
30
|
+
return event_dict
|
|
31
|
+
|
|
32
|
+
|
|
25
33
|
class AddPrefixedEnvVars:
|
|
26
34
|
def __init__(self, prefix: str) -> None:
|
|
27
35
|
self.kv = {
|
|
@@ -68,6 +76,7 @@ class TapForMetadata:
|
|
|
68
76
|
# shared config, for both logging and structlog
|
|
69
77
|
|
|
70
78
|
shared_processors = [
|
|
79
|
+
add_thread_name,
|
|
71
80
|
structlog.stdlib.add_logger_name,
|
|
72
81
|
structlog.stdlib.add_log_level,
|
|
73
82
|
structlog.stdlib.PositionalArgumentsFormatter(),
|
langgraph_api/metadata.py
CHANGED
|
@@ -10,6 +10,8 @@ from langgraph_api.config import (
|
|
|
10
10
|
LANGGRAPH_CLOUD_LICENSE_KEY,
|
|
11
11
|
LANGSMITH_API_KEY,
|
|
12
12
|
LANGSMITH_AUTH_ENDPOINT,
|
|
13
|
+
USES_CUSTOM_APP,
|
|
14
|
+
USES_INDEXING,
|
|
13
15
|
)
|
|
14
16
|
from langgraph_api.http import http_request
|
|
15
17
|
from langgraph_license.validation import plus_features_enabled
|
|
@@ -87,6 +89,8 @@ async def metadata_loop() -> None:
|
|
|
87
89
|
"langgraph.platform.variant": VARIANT,
|
|
88
90
|
"langgraph.platform.host": HOST,
|
|
89
91
|
"langgraph.platform.plan": PLAN,
|
|
92
|
+
"user_app.uses_indexing": USES_INDEXING,
|
|
93
|
+
"user_app.uses_custom_app": USES_CUSTOM_APP,
|
|
90
94
|
},
|
|
91
95
|
"measures": {
|
|
92
96
|
"langgraph.platform.runs": runs,
|
|
@@ -106,5 +110,5 @@ async def metadata_loop() -> None:
|
|
|
106
110
|
incr_runs(incr=runs)
|
|
107
111
|
incr_nodes("", incr=nodes)
|
|
108
112
|
FROM_TIMESTAMP = from_timestamp
|
|
109
|
-
logger.
|
|
113
|
+
await logger.ainfo("Metadata submission skipped.", error=str(e))
|
|
110
114
|
await asyncio.sleep(INTERVAL)
|
|
@@ -78,7 +78,7 @@ class AccessLoggerMiddleware:
|
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
HEADERS_IGNORE = {"authorization", "cookie", "set-cookie", "x-api-key"}
|
|
81
|
+
HEADERS_IGNORE = {b"authorization", b"cookie", b"set-cookie", b"x-api-key"}
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def _headers_to_dict(headers: list[tuple[bytes, bytes]] | None) -> dict[str, str]:
|
langgraph_api/patch.py
CHANGED
|
@@ -26,6 +26,8 @@ async def StreamingResponse_stream_response(self, send: Send) -> None:
|
|
|
26
26
|
}
|
|
27
27
|
)
|
|
28
28
|
async for chunk in self.body_iterator:
|
|
29
|
+
if chunk is None:
|
|
30
|
+
continue
|
|
29
31
|
if not isinstance(chunk, (bytes, bytearray, memoryview)): # noqa: UP038
|
|
30
32
|
chunk = chunk.encode(self.charset)
|
|
31
33
|
await send({"type": "http.response.body", "body": chunk, "more_body": True})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import http.server
|
|
3
|
+
import json
|
|
4
|
+
import logging.config
|
|
5
|
+
import os
|
|
6
|
+
import pathlib
|
|
7
|
+
|
|
8
|
+
import structlog
|
|
9
|
+
import uvloop
|
|
10
|
+
|
|
11
|
+
from langgraph_api.lifespan import lifespan
|
|
12
|
+
|
|
13
|
+
logger = structlog.stdlib.get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def healthcheck_server():
|
|
17
|
+
port = int(os.getenv("PORT", "8080"))
|
|
18
|
+
ok = json.dumps({"status": "ok"}).encode()
|
|
19
|
+
ok_len = str(len(ok))
|
|
20
|
+
|
|
21
|
+
class HealthHandler(http.server.SimpleHTTPRequestHandler):
|
|
22
|
+
def do_GET(self):
|
|
23
|
+
if self.path == "/ok":
|
|
24
|
+
self.send_response(200)
|
|
25
|
+
self.send_header("Content-Type", "application/json")
|
|
26
|
+
self.send_header("Content-Length", ok_len)
|
|
27
|
+
self.end_headers()
|
|
28
|
+
self.wfile.write(ok)
|
|
29
|
+
else:
|
|
30
|
+
self.send_error(http.HTTPStatus.NOT_FOUND)
|
|
31
|
+
|
|
32
|
+
with http.server.ThreadingHTTPServer(("0.0.0.0", port), HealthHandler) as httpd:
|
|
33
|
+
logger.info(f"Health server started at http://0.0.0.0:{port}")
|
|
34
|
+
try:
|
|
35
|
+
await asyncio.to_thread(httpd.serve_forever)
|
|
36
|
+
finally:
|
|
37
|
+
httpd.shutdown()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def entrypoint():
|
|
41
|
+
tasks: set[asyncio.Task] = set()
|
|
42
|
+
# start simple http server for health checks
|
|
43
|
+
tasks.add(asyncio.create_task(healthcheck_server()))
|
|
44
|
+
# start queue and associated tasks
|
|
45
|
+
async with lifespan(None, with_cron_scheduler=False, taskset=tasks):
|
|
46
|
+
# run forever, error if any tasks fail
|
|
47
|
+
try:
|
|
48
|
+
await asyncio.gather(*tasks)
|
|
49
|
+
except asyncio.CancelledError:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
# set up logging
|
|
55
|
+
with open(pathlib.Path(__file__).parent.parent / "logging.json") as file:
|
|
56
|
+
loaded_config = json.load(file)
|
|
57
|
+
logging.config.dictConfig(loaded_config)
|
|
58
|
+
|
|
59
|
+
# set up uvloop
|
|
60
|
+
uvloop.install()
|
|
61
|
+
|
|
62
|
+
# run the entrypoint
|
|
63
|
+
asyncio.run(entrypoint())
|
langgraph_api/schema.py
CHANGED
langgraph_api/stream.py
CHANGED
|
@@ -111,6 +111,7 @@ async def astream_state(
|
|
|
111
111
|
await stack.enter_async_context(conn.pipeline())
|
|
112
112
|
# extract args from run
|
|
113
113
|
kwargs = run["kwargs"].copy()
|
|
114
|
+
kwargs.pop("webhook", None)
|
|
114
115
|
subgraphs = kwargs.get("subgraphs", False)
|
|
115
116
|
temporary = kwargs.pop("temporary", False)
|
|
116
117
|
config = kwargs.pop("config")
|
langgraph_api/webhook.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from datetime import UTC, datetime
|
|
2
|
+
|
|
3
|
+
import structlog
|
|
4
|
+
|
|
5
|
+
from langgraph_api.http import get_http_client, get_loopback_client
|
|
6
|
+
from langgraph_api.worker import WorkerResult
|
|
7
|
+
|
|
8
|
+
logger = structlog.stdlib.get_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def call_webhook(result: "WorkerResult") -> None:
|
|
12
|
+
checkpoint = result["checkpoint"]
|
|
13
|
+
payload = {
|
|
14
|
+
**result["run"],
|
|
15
|
+
"status": result["status"],
|
|
16
|
+
"run_started_at": result["run_started_at"],
|
|
17
|
+
"run_ended_at": result["run_ended_at"],
|
|
18
|
+
"webhook_sent_at": datetime.now(UTC).isoformat(),
|
|
19
|
+
"values": checkpoint["values"] if checkpoint else None,
|
|
20
|
+
}
|
|
21
|
+
if exception := result["exception"]:
|
|
22
|
+
payload["error"] = str(exception)
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
webhook = result["webhook"]
|
|
26
|
+
if webhook.startswith("/"):
|
|
27
|
+
# Call into this own app
|
|
28
|
+
webhook_client = get_loopback_client()
|
|
29
|
+
else:
|
|
30
|
+
webhook_client = get_http_client()
|
|
31
|
+
await webhook_client.post(webhook, json=payload, total_timeout=20)
|
|
32
|
+
await logger.ainfo(
|
|
33
|
+
"Background worker called webhook",
|
|
34
|
+
webhook=result["webhook"],
|
|
35
|
+
run_id=result["run"]["run_id"],
|
|
36
|
+
)
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
logger.exception(
|
|
39
|
+
f"Background worker failed to call webhook {result['webhook']}",
|
|
40
|
+
exc_info=exc,
|
|
41
|
+
webhook=result["webhook"],
|
|
42
|
+
)
|
|
@@ -6,14 +6,17 @@ from typing import TypedDict, cast
|
|
|
6
6
|
|
|
7
7
|
import structlog
|
|
8
8
|
from langgraph.pregel.debug import CheckpointPayload, TaskResultPayload
|
|
9
|
+
from starlette.exceptions import HTTPException
|
|
9
10
|
|
|
10
11
|
from langgraph_api.auth.custom import SimpleUser, normalize_user
|
|
11
|
-
from langgraph_api.config import
|
|
12
|
+
from langgraph_api.config import (
|
|
13
|
+
BG_JOB_MAX_RETRIES,
|
|
14
|
+
BG_JOB_TIMEOUT_SECS,
|
|
15
|
+
)
|
|
12
16
|
from langgraph_api.errors import (
|
|
13
17
|
UserInterrupt,
|
|
14
18
|
UserRollback,
|
|
15
19
|
)
|
|
16
|
-
from langgraph_api.http import get_http_client
|
|
17
20
|
from langgraph_api.js.errors import RemoteException
|
|
18
21
|
from langgraph_api.metadata import incr_runs
|
|
19
22
|
from langgraph_api.schema import Run
|
|
@@ -33,135 +36,6 @@ except ImportError:
|
|
|
33
36
|
|
|
34
37
|
logger = structlog.stdlib.get_logger(__name__)
|
|
35
38
|
|
|
36
|
-
WORKERS: set[asyncio.Task] = set()
|
|
37
|
-
WEBHOOKS: set[asyncio.Task] = set()
|
|
38
|
-
MAX_RETRY_ATTEMPTS = 3
|
|
39
|
-
SHUTDOWN_GRACE_PERIOD_SECS = 5
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def ms(after: datetime, before: datetime) -> int:
|
|
43
|
-
return int((after - before).total_seconds() * 1000)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
async def queue(concurrency: int, timeout: float):
|
|
47
|
-
loop = asyncio.get_running_loop()
|
|
48
|
-
last_stats_secs: int | None = None
|
|
49
|
-
last_sweep_secs: int | None = None
|
|
50
|
-
semaphore = asyncio.Semaphore(concurrency)
|
|
51
|
-
|
|
52
|
-
def cleanup(task: asyncio.Task):
|
|
53
|
-
WORKERS.remove(task)
|
|
54
|
-
semaphore.release()
|
|
55
|
-
try:
|
|
56
|
-
result: WorkerResult | None = task.result()
|
|
57
|
-
exc = task.exception()
|
|
58
|
-
if exc:
|
|
59
|
-
logger.exception("Background worker failed", exc_info=exc)
|
|
60
|
-
if result and result["webhook"]:
|
|
61
|
-
checkpoint = result["checkpoint"]
|
|
62
|
-
payload = {
|
|
63
|
-
**result["run"],
|
|
64
|
-
"status": result["status"],
|
|
65
|
-
"run_started_at": result["run_started_at"],
|
|
66
|
-
"run_ended_at": result["run_ended_at"],
|
|
67
|
-
"webhook_sent_at": datetime.now(UTC).isoformat(),
|
|
68
|
-
"values": checkpoint["values"] if checkpoint else None,
|
|
69
|
-
}
|
|
70
|
-
if exception := result["exception"]:
|
|
71
|
-
payload["error"] = str(exception)
|
|
72
|
-
|
|
73
|
-
async def _call_webhook() -> None:
|
|
74
|
-
try:
|
|
75
|
-
await get_http_client().post(
|
|
76
|
-
result["webhook"], json=payload, total_timeout=20
|
|
77
|
-
)
|
|
78
|
-
except Exception as exc:
|
|
79
|
-
logger.exception(
|
|
80
|
-
f"Background worker failed to call webhook {result['webhook']}",
|
|
81
|
-
exc_info=exc,
|
|
82
|
-
webhook=result["webhook"],
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
hook_task = asyncio.create_task(
|
|
86
|
-
_call_webhook(),
|
|
87
|
-
name=f"webhook-{result['run']['run_id']}",
|
|
88
|
-
)
|
|
89
|
-
WEBHOOKS.add(hook_task)
|
|
90
|
-
hook_task.add_done_callback(WEBHOOKS.remove)
|
|
91
|
-
|
|
92
|
-
except asyncio.CancelledError:
|
|
93
|
-
pass
|
|
94
|
-
except Exception as exc:
|
|
95
|
-
logger.exception("Background worker cleanup failed", exc_info=exc)
|
|
96
|
-
|
|
97
|
-
await logger.ainfo(f"Starting {concurrency} background workers")
|
|
98
|
-
try:
|
|
99
|
-
tup: tuple[Run, int] | None = None
|
|
100
|
-
while True:
|
|
101
|
-
try:
|
|
102
|
-
# check if we need to sweep runs
|
|
103
|
-
do_sweep = (
|
|
104
|
-
last_sweep_secs is None
|
|
105
|
-
or loop.time() - last_sweep_secs > BG_JOB_HEARTBEAT * 2
|
|
106
|
-
)
|
|
107
|
-
# check if we need to update stats
|
|
108
|
-
if calc_stats := (
|
|
109
|
-
last_stats_secs is None
|
|
110
|
-
or loop.time() - last_stats_secs > STATS_INTERVAL_SECS
|
|
111
|
-
):
|
|
112
|
-
last_stats_secs = loop.time()
|
|
113
|
-
active = len(WORKERS)
|
|
114
|
-
await logger.ainfo(
|
|
115
|
-
"Worker stats",
|
|
116
|
-
max=concurrency,
|
|
117
|
-
available=concurrency - active,
|
|
118
|
-
active=active,
|
|
119
|
-
)
|
|
120
|
-
# wait for semaphore to respect concurrency
|
|
121
|
-
await semaphore.acquire()
|
|
122
|
-
exit = AsyncExitStack()
|
|
123
|
-
# skip the wait, if 1st time, or got a run last time
|
|
124
|
-
wait = tup is None and last_stats_secs is not None
|
|
125
|
-
# try to get a run, handle it
|
|
126
|
-
if tup := await exit.enter_async_context(Runs.next(wait=wait)):
|
|
127
|
-
run_, attempt_ = tup
|
|
128
|
-
task = asyncio.create_task(
|
|
129
|
-
worker(timeout, exit, run_, attempt_),
|
|
130
|
-
name=f"run-{run_['run_id']}-attempt-{attempt_}",
|
|
131
|
-
)
|
|
132
|
-
task.add_done_callback(cleanup)
|
|
133
|
-
WORKERS.add(task)
|
|
134
|
-
else:
|
|
135
|
-
semaphore.release()
|
|
136
|
-
await exit.aclose()
|
|
137
|
-
# run stats and sweep if needed
|
|
138
|
-
if calc_stats or do_sweep:
|
|
139
|
-
async with connect() as conn:
|
|
140
|
-
# update stats if needed
|
|
141
|
-
if calc_stats:
|
|
142
|
-
stats = await Runs.stats(conn)
|
|
143
|
-
await logger.ainfo("Queue stats", **stats)
|
|
144
|
-
# sweep runs if needed
|
|
145
|
-
if do_sweep:
|
|
146
|
-
last_sweep_secs = loop.time()
|
|
147
|
-
run_ids = await Runs.sweep(conn)
|
|
148
|
-
logger.info("Sweeped runs", run_ids=run_ids)
|
|
149
|
-
except Exception as exc:
|
|
150
|
-
# keep trying to run the scheduler indefinitely
|
|
151
|
-
logger.exception("Background worker scheduler failed", exc_info=exc)
|
|
152
|
-
semaphore.release()
|
|
153
|
-
await exit.aclose()
|
|
154
|
-
finally:
|
|
155
|
-
logger.info("Shutting down background workers")
|
|
156
|
-
for task in WORKERS:
|
|
157
|
-
task.cancel()
|
|
158
|
-
for task in WEBHOOKS:
|
|
159
|
-
task.cancel()
|
|
160
|
-
await asyncio.wait_for(
|
|
161
|
-
asyncio.gather(*WORKERS, *WEBHOOKS, return_exceptions=True),
|
|
162
|
-
SHUTDOWN_GRACE_PERIOD_SECS,
|
|
163
|
-
)
|
|
164
|
-
|
|
165
39
|
|
|
166
40
|
class WorkerResult(TypedDict):
|
|
167
41
|
checkpoint: CheckpointPayload | None
|
|
@@ -181,32 +55,21 @@ async def set_auth_ctx_for_run(
|
|
|
181
55
|
try:
|
|
182
56
|
user = run_kwargs["config"]["configurable"]["langgraph_auth_user"]
|
|
183
57
|
permissions = run_kwargs["config"]["configurable"]["langgraph_auth_permissions"]
|
|
184
|
-
|
|
185
|
-
user = normalize_user(user)
|
|
186
|
-
async with with_user(user, permissions):
|
|
187
|
-
yield None
|
|
188
|
-
else:
|
|
189
|
-
yield None
|
|
190
|
-
|
|
191
|
-
except KeyError:
|
|
192
|
-
if user_id is not None:
|
|
193
|
-
await logger.ainfo(
|
|
194
|
-
"Setting auth to backup user_id",
|
|
195
|
-
user_id=user_id,
|
|
196
|
-
)
|
|
197
|
-
async with with_user(SimpleUser(user_id)):
|
|
198
|
-
yield None
|
|
199
|
-
else:
|
|
200
|
-
yield None
|
|
58
|
+
user = normalize_user(user)
|
|
201
59
|
except Exception:
|
|
202
|
-
|
|
60
|
+
user = SimpleUser(user_id) if user_id is not None else None
|
|
61
|
+
permissions = None
|
|
62
|
+
if user is not None:
|
|
63
|
+
async with with_user(user, permissions):
|
|
64
|
+
yield None
|
|
65
|
+
else:
|
|
66
|
+
yield None
|
|
203
67
|
|
|
204
68
|
|
|
205
69
|
async def worker(
|
|
206
|
-
timeout: float,
|
|
207
|
-
exit: AsyncExitStack,
|
|
208
70
|
run: Run,
|
|
209
71
|
attempt: int,
|
|
72
|
+
main_loop: asyncio.AbstractEventLoop,
|
|
210
73
|
) -> WorkerResult:
|
|
211
74
|
run_id = run["run_id"]
|
|
212
75
|
if attempt == 1:
|
|
@@ -214,15 +77,14 @@ async def worker(
|
|
|
214
77
|
checkpoint: CheckpointPayload | None = None
|
|
215
78
|
exception: Exception | None = None
|
|
216
79
|
status: str | None = None
|
|
217
|
-
webhook = run["kwargs"].
|
|
80
|
+
webhook = run["kwargs"].get("webhook", None)
|
|
218
81
|
run_started_at = datetime.now(UTC)
|
|
219
82
|
run_ended_at: str | None = None
|
|
220
83
|
|
|
221
84
|
async with (
|
|
222
85
|
connect() as conn,
|
|
223
86
|
set_auth_ctx_for_run(run["kwargs"]),
|
|
224
|
-
Runs.enter(run_id) as done,
|
|
225
|
-
exit,
|
|
87
|
+
Runs.enter(run_id, main_loop) as done,
|
|
226
88
|
):
|
|
227
89
|
temporary = run["kwargs"].get("temporary", False)
|
|
228
90
|
run_created_at = run["created_at"].isoformat()
|
|
@@ -247,7 +109,7 @@ async def worker(
|
|
|
247
109
|
break
|
|
248
110
|
|
|
249
111
|
try:
|
|
250
|
-
if attempt >
|
|
112
|
+
if attempt > BG_JOB_MAX_RETRIES:
|
|
251
113
|
raise RuntimeError(f"Run {run['run_id']} exceeded max attempts")
|
|
252
114
|
if temporary:
|
|
253
115
|
stream = astream_state(
|
|
@@ -263,7 +125,7 @@ async def worker(
|
|
|
263
125
|
on_checkpoint=on_checkpoint,
|
|
264
126
|
on_task_result=on_task_result,
|
|
265
127
|
)
|
|
266
|
-
await asyncio.wait_for(consume(stream, run_id),
|
|
128
|
+
await asyncio.wait_for(consume(stream, run_id), BG_JOB_TIMEOUT_SECS)
|
|
267
129
|
run_ended_at = datetime.now(UTC).isoformat()
|
|
268
130
|
await logger.ainfo(
|
|
269
131
|
"Background run succeeded",
|
|
@@ -294,17 +156,18 @@ async def worker(
|
|
|
294
156
|
exception = e
|
|
295
157
|
status = "rollback"
|
|
296
158
|
run_ended_at = datetime.now(UTC).isoformat()
|
|
297
|
-
await logger.ainfo(
|
|
298
|
-
"Background run rolled back",
|
|
299
|
-
run_id=str(run_id),
|
|
300
|
-
run_attempt=attempt,
|
|
301
|
-
run_created_at=run_created_at,
|
|
302
|
-
run_started_at=run_started_at.isoformat(),
|
|
303
|
-
run_ended_at=run_ended_at,
|
|
304
|
-
run_exec_ms=ms(datetime.now(UTC), run_started_at),
|
|
305
|
-
)
|
|
306
159
|
try:
|
|
307
160
|
await Runs.delete(conn, run_id, thread_id=run["thread_id"])
|
|
161
|
+
await logger.ainfo(
|
|
162
|
+
"Background run rolled back",
|
|
163
|
+
run_id=str(run_id),
|
|
164
|
+
run_attempt=attempt,
|
|
165
|
+
run_created_at=run_created_at,
|
|
166
|
+
run_started_at=run_started_at.isoformat(),
|
|
167
|
+
run_ended_at=run_ended_at,
|
|
168
|
+
run_exec_ms=ms(datetime.now(UTC), run_started_at),
|
|
169
|
+
)
|
|
170
|
+
|
|
308
171
|
except InFailedSqlTransaction as e:
|
|
309
172
|
await logger.ainfo(
|
|
310
173
|
"Ignoring rollback error",
|
|
@@ -316,6 +179,17 @@ async def worker(
|
|
|
316
179
|
# We need to clean up the transaction early if we want to
|
|
317
180
|
# update the thread status with the same connection
|
|
318
181
|
await exit.aclose()
|
|
182
|
+
except HTTPException as e:
|
|
183
|
+
if e.status_code == 404:
|
|
184
|
+
await logger.ainfo(
|
|
185
|
+
"Ignoring rollback error for missing run",
|
|
186
|
+
run_id=str(run_id),
|
|
187
|
+
run_attempt=attempt,
|
|
188
|
+
run_created_at=run_created_at,
|
|
189
|
+
)
|
|
190
|
+
else:
|
|
191
|
+
raise
|
|
192
|
+
|
|
319
193
|
checkpoint = None # reset the checkpoint
|
|
320
194
|
except UserInterrupt as e:
|
|
321
195
|
exception = e
|
|
@@ -367,7 +241,15 @@ async def worker(
|
|
|
367
241
|
if temporary:
|
|
368
242
|
await Threads.delete(conn, run["thread_id"])
|
|
369
243
|
else:
|
|
370
|
-
|
|
244
|
+
try:
|
|
245
|
+
await Threads.set_status(conn, run["thread_id"], checkpoint, exception)
|
|
246
|
+
except HTTPException as e:
|
|
247
|
+
if e.status_code == 404:
|
|
248
|
+
await logger.ainfo(
|
|
249
|
+
"Ignoring set_status error for missing thread", exc=str(e)
|
|
250
|
+
)
|
|
251
|
+
else:
|
|
252
|
+
raise
|
|
371
253
|
# Note we don't handle asyncio.CancelledError here, as we want to
|
|
372
254
|
# let it bubble up and rollback db transaction, thus marking the run
|
|
373
255
|
# as available to be picked up by another worker
|
|
@@ -380,3 +262,7 @@ async def worker(
|
|
|
380
262
|
"exception": exception,
|
|
381
263
|
"webhook": webhook,
|
|
382
264
|
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def ms(after: datetime, before: datetime) -> int:
|
|
268
|
+
return int((after - before).total_seconds() * 1000)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: langgraph-api
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.28rc1
|
|
4
4
|
Summary:
|
|
5
5
|
License: Elastic-2.0
|
|
6
6
|
Author: Nuno Campos
|
|
@@ -11,26 +11,26 @@ Classifier: Programming Language :: Python :: 3
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
13
|
Requires-Dist: cryptography (>=43.0.3,<44.0.0)
|
|
14
|
-
Requires-Dist: httpx (>=0.
|
|
15
|
-
Requires-Dist: jsonschema-rs (>=0.
|
|
14
|
+
Requires-Dist: httpx (>=0.25.0)
|
|
15
|
+
Requires-Dist: jsonschema-rs (>=0.20.0,<0.21.0)
|
|
16
16
|
Requires-Dist: langchain-core (>=0.2.38,<0.4.0)
|
|
17
|
-
Requires-Dist: langgraph (>=0.2.56,<0.
|
|
17
|
+
Requires-Dist: langgraph (>=0.2.56,<0.4.0)
|
|
18
18
|
Requires-Dist: langgraph-checkpoint (>=2.0.15,<3.0)
|
|
19
19
|
Requires-Dist: langgraph-sdk (>=0.1.53,<0.2.0)
|
|
20
20
|
Requires-Dist: langsmith (>=0.1.63,<0.4.0)
|
|
21
|
-
Requires-Dist: orjson (>=3.
|
|
21
|
+
Requires-Dist: orjson (>=3.9.7)
|
|
22
22
|
Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
|
|
23
23
|
Requires-Dist: sse-starlette (>=2.1.0,<2.2.0)
|
|
24
24
|
Requires-Dist: starlette (>=0.38.6)
|
|
25
|
-
Requires-Dist: structlog (>=24.
|
|
26
|
-
Requires-Dist: tenacity (>=8.
|
|
25
|
+
Requires-Dist: structlog (>=24.1.0,<26)
|
|
26
|
+
Requires-Dist: tenacity (>=8.0.0)
|
|
27
27
|
Requires-Dist: uvicorn (>=0.26.0)
|
|
28
28
|
Requires-Dist: watchfiles (>=0.13)
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
|
|
31
31
|
# LangGraph API
|
|
32
32
|
|
|
33
|
-
This package implements the LangGraph API for rapid development and testing. Build and iterate on LangGraph agents with a tight feedback loop. The
|
|
33
|
+
This package implements the LangGraph API for rapid development and testing. Build and iterate on LangGraph agents with a tight feedback loop. The server is backed by a predominently in-memory data store that is persisted to local disk when the server is restarted.
|
|
34
34
|
|
|
35
35
|
For production use, see the various [deployment options](https://langchain-ai.github.io/langgraph/concepts/deployment_options/) for the LangGraph API, which are backed by a production-grade database.
|
|
36
36
|
|