langgraph-api 0.0.48__py3-none-any.whl → 0.1.0__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/__init__.py +2 -2
- langgraph_api/api/assistants.py +3 -3
- langgraph_api/api/meta.py +9 -11
- langgraph_api/api/runs.py +3 -3
- langgraph_api/api/store.py +2 -2
- langgraph_api/api/threads.py +3 -3
- langgraph_api/cli.py +3 -1
- langgraph_api/config.py +3 -0
- langgraph_api/cron_scheduler.py +3 -3
- langgraph_api/graph.py +2 -2
- langgraph_api/js/remote.py +3 -3
- langgraph_api/metadata.py +7 -0
- langgraph_api/models/run.py +10 -1
- langgraph_api/queue_entrypoint.py +1 -1
- langgraph_api/server.py +2 -2
- langgraph_api/stream.py +3 -3
- langgraph_api/thread_ttl.py +2 -2
- langgraph_api/worker.py +3 -3
- {langgraph_api-0.0.48.dist-info → langgraph_api-0.1.0.dist-info}/METADATA +1 -1
- {langgraph_api-0.0.48.dist-info → langgraph_api-0.1.0.dist-info}/RECORD +25 -33
- langgraph_runtime/__init__.py +39 -0
- langgraph_api/lifespan.py +0 -74
- langgraph_storage/__init__.py +0 -0
- langgraph_storage/checkpoint.py +0 -123
- langgraph_storage/database.py +0 -200
- langgraph_storage/inmem_stream.py +0 -109
- langgraph_storage/ops.py +0 -2172
- langgraph_storage/queue.py +0 -186
- langgraph_storage/retry.py +0 -31
- langgraph_storage/store.py +0 -100
- {langgraph_api-0.0.48.dist-info → langgraph_api-0.1.0.dist-info}/LICENSE +0 -0
- {langgraph_api-0.0.48.dist-info → langgraph_api-0.1.0.dist-info}/WHEEL +0 -0
- {langgraph_api-0.0.48.dist-info → langgraph_api-0.1.0.dist-info}/entry_points.txt +0 -0
langgraph_storage/queue.py
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import os
|
|
3
|
-
|
|
4
|
-
import structlog
|
|
5
|
-
from blockbuster import BlockBuster
|
|
6
|
-
from langsmith import env as ls_env
|
|
7
|
-
|
|
8
|
-
from langgraph_api.config import (
|
|
9
|
-
BG_JOB_HEARTBEAT,
|
|
10
|
-
N_JOBS_PER_WORKER,
|
|
11
|
-
STATS_INTERVAL_SECS,
|
|
12
|
-
)
|
|
13
|
-
from langgraph_api.graph import is_js_graph
|
|
14
|
-
from langgraph_api.schema import Run
|
|
15
|
-
from langgraph_api.webhook import call_webhook
|
|
16
|
-
from langgraph_api.worker import WorkerResult, worker
|
|
17
|
-
from langgraph_storage.database import connect
|
|
18
|
-
from langgraph_storage.ops import Runs
|
|
19
|
-
|
|
20
|
-
logger = structlog.stdlib.get_logger(__name__)
|
|
21
|
-
|
|
22
|
-
WORKERS: set[asyncio.Task] = set()
|
|
23
|
-
SHUTDOWN_GRACE_PERIOD_SECS = 5
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
async def queue():
|
|
27
|
-
concurrency = N_JOBS_PER_WORKER
|
|
28
|
-
loop = asyncio.get_running_loop()
|
|
29
|
-
last_stats_secs: int | None = None
|
|
30
|
-
last_sweep_secs: int | None = None
|
|
31
|
-
semaphore = asyncio.Semaphore(concurrency)
|
|
32
|
-
WEBHOOKS: set[asyncio.Task] = set()
|
|
33
|
-
enable_blocking = os.getenv("LANGGRAPH_ALLOW_BLOCKING", "false").lower() == "true"
|
|
34
|
-
|
|
35
|
-
# raise exceptions when a blocking call is detected inside an async function
|
|
36
|
-
if enable_blocking:
|
|
37
|
-
bb = None
|
|
38
|
-
await logger.awarning(
|
|
39
|
-
"Heads up: You've set --allow-blocking, which allows synchronous blocking I/O operations."
|
|
40
|
-
" Be aware that blocking code in one run may tie up the shared event loop"
|
|
41
|
-
" and slow down ALL other server operations. For best performance, either convert blocking"
|
|
42
|
-
" code to async patterns or set BG_JOB_ISOLATED_LOOPS=true in production"
|
|
43
|
-
" to isolate each run in its own event loop."
|
|
44
|
-
)
|
|
45
|
-
else:
|
|
46
|
-
bb = _enable_blockbuster()
|
|
47
|
-
|
|
48
|
-
def cleanup(task: asyncio.Task):
|
|
49
|
-
WORKERS.remove(task)
|
|
50
|
-
semaphore.release()
|
|
51
|
-
try:
|
|
52
|
-
if task.cancelled():
|
|
53
|
-
return
|
|
54
|
-
exc = task.exception()
|
|
55
|
-
if exc and not isinstance(exc, asyncio.CancelledError):
|
|
56
|
-
logger.exception(
|
|
57
|
-
f"Background worker failed for task {task}", exc_info=exc
|
|
58
|
-
)
|
|
59
|
-
return
|
|
60
|
-
result: WorkerResult | None = task.result()
|
|
61
|
-
if result and result["webhook"]:
|
|
62
|
-
hook_task = loop.create_task(
|
|
63
|
-
call_webhook(result),
|
|
64
|
-
name=f"webhook-{result['run']['run_id']}",
|
|
65
|
-
)
|
|
66
|
-
WEBHOOKS.add(hook_task)
|
|
67
|
-
hook_task.add_done_callback(WEBHOOKS.remove)
|
|
68
|
-
except asyncio.CancelledError:
|
|
69
|
-
pass
|
|
70
|
-
except Exception as exc:
|
|
71
|
-
logger.exception("Background worker cleanup failed", exc_info=exc)
|
|
72
|
-
|
|
73
|
-
await logger.ainfo(f"Starting {concurrency} background workers")
|
|
74
|
-
try:
|
|
75
|
-
run: Run | None = None
|
|
76
|
-
while True:
|
|
77
|
-
try:
|
|
78
|
-
# check if we need to sweep runs
|
|
79
|
-
do_sweep = (
|
|
80
|
-
last_sweep_secs is None
|
|
81
|
-
or loop.time() - last_sweep_secs > BG_JOB_HEARTBEAT * 2
|
|
82
|
-
)
|
|
83
|
-
# check if we need to update stats
|
|
84
|
-
if calc_stats := (
|
|
85
|
-
last_stats_secs is None
|
|
86
|
-
or loop.time() - last_stats_secs > STATS_INTERVAL_SECS
|
|
87
|
-
):
|
|
88
|
-
last_stats_secs = loop.time()
|
|
89
|
-
active = len(WORKERS)
|
|
90
|
-
await logger.ainfo(
|
|
91
|
-
"Worker stats",
|
|
92
|
-
max=concurrency,
|
|
93
|
-
available=concurrency - active,
|
|
94
|
-
active=active,
|
|
95
|
-
)
|
|
96
|
-
# wait for semaphore to respect concurrency
|
|
97
|
-
await semaphore.acquire()
|
|
98
|
-
# skip the wait, if 1st time, or got a run last time
|
|
99
|
-
wait = run is None and last_stats_secs is not None
|
|
100
|
-
# try to get a run, handle it
|
|
101
|
-
run = None
|
|
102
|
-
async for run, attempt in Runs.next(wait=wait, limit=1):
|
|
103
|
-
graph_id = (
|
|
104
|
-
run["kwargs"]
|
|
105
|
-
.get("config", {})
|
|
106
|
-
.get("configurable", {})
|
|
107
|
-
.get("graph_id")
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
if graph_id and is_js_graph(graph_id):
|
|
111
|
-
task_name = f"js-run-{run['run_id']}-attempt-{attempt}"
|
|
112
|
-
else:
|
|
113
|
-
task_name = f"run-{run['run_id']}-attempt-{attempt}"
|
|
114
|
-
task = asyncio.create_task(
|
|
115
|
-
worker(run, attempt, loop),
|
|
116
|
-
name=task_name,
|
|
117
|
-
)
|
|
118
|
-
task.add_done_callback(cleanup)
|
|
119
|
-
WORKERS.add(task)
|
|
120
|
-
else:
|
|
121
|
-
semaphore.release()
|
|
122
|
-
# run stats and sweep if needed
|
|
123
|
-
if calc_stats or do_sweep:
|
|
124
|
-
async with connect() as conn:
|
|
125
|
-
# update stats if needed
|
|
126
|
-
if calc_stats:
|
|
127
|
-
stats = await Runs.stats(conn)
|
|
128
|
-
await logger.ainfo("Queue stats", **stats)
|
|
129
|
-
# sweep runs if needed
|
|
130
|
-
if do_sweep:
|
|
131
|
-
last_sweep_secs = loop.time()
|
|
132
|
-
run_ids = await Runs.sweep(conn)
|
|
133
|
-
logger.info("Sweeped runs", run_ids=run_ids)
|
|
134
|
-
except Exception as exc:
|
|
135
|
-
# keep trying to run the scheduler indefinitely
|
|
136
|
-
logger.exception("Background worker scheduler failed", exc_info=exc)
|
|
137
|
-
semaphore.release()
|
|
138
|
-
await exit.aclose()
|
|
139
|
-
finally:
|
|
140
|
-
if bb:
|
|
141
|
-
bb.deactivate()
|
|
142
|
-
logger.info("Shutting down background workers")
|
|
143
|
-
for task in WORKERS:
|
|
144
|
-
task.cancel("Shutting down background workers.")
|
|
145
|
-
for task in WEBHOOKS:
|
|
146
|
-
task.cancel("Shutting down webhooks for background workers.")
|
|
147
|
-
await asyncio.wait_for(
|
|
148
|
-
asyncio.gather(*WORKERS, *WEBHOOKS, return_exceptions=True),
|
|
149
|
-
SHUTDOWN_GRACE_PERIOD_SECS,
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def _enable_blockbuster() -> BlockBuster:
|
|
154
|
-
ls_env.get_runtime_environment() # this gets cached
|
|
155
|
-
bb = BlockBuster(excluded_modules=[])
|
|
156
|
-
bb.activate()
|
|
157
|
-
# Note, we've cached this call in langsmith==0.3.21 so it shouldn't raise anyway
|
|
158
|
-
# but we don't want to raise teh minbound just for that.
|
|
159
|
-
bb.functions["os.stat"].deactivate()
|
|
160
|
-
for function in bb.functions:
|
|
161
|
-
if function.startswith("os.path."):
|
|
162
|
-
bb.functions[function].deactivate()
|
|
163
|
-
for module, func in (
|
|
164
|
-
# Note, we've cached this call in langsmith==0.3.21 so it shouldn't raise anyway
|
|
165
|
-
# but we don't want to raise teh minbound just for that.
|
|
166
|
-
("langsmith/client.py", "_default_retry_config"),
|
|
167
|
-
# Only triggers in python 3.11 for getting subgraphs
|
|
168
|
-
# Will be unnecessary once we cache the assistant schemas
|
|
169
|
-
("langgraph/pregel/utils.py", "get_function_nonlocals"),
|
|
170
|
-
("importlib/metadata/__init__.py", "metadata"),
|
|
171
|
-
("importlib/metadata/__init__.py", "read_text"),
|
|
172
|
-
):
|
|
173
|
-
bb.functions["io.TextIOWrapper.read"].can_block_in(module, func)
|
|
174
|
-
|
|
175
|
-
bb.functions["os.path.abspath"].can_block_in("inspect.py", "getmodule")
|
|
176
|
-
|
|
177
|
-
for module, func in (
|
|
178
|
-
("uvicorn/lifespan/on.py", "startup"),
|
|
179
|
-
("uvicorn/lifespan/on.py", "shutdown"),
|
|
180
|
-
("ansitowin32.py", "write_plain_text"),
|
|
181
|
-
("logging/__init__.py", "flush"),
|
|
182
|
-
("logging/__init__.py", "emit"),
|
|
183
|
-
):
|
|
184
|
-
bb.functions["io.TextIOWrapper.write"].can_block_in(module, func)
|
|
185
|
-
bb.functions["io.BufferedWriter.write"].can_block_in(module, func)
|
|
186
|
-
return bb
|
langgraph_storage/retry.py
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import functools
|
|
3
|
-
from collections.abc import Callable
|
|
4
|
-
from typing import ParamSpec, TypeVar
|
|
5
|
-
|
|
6
|
-
P = ParamSpec("P")
|
|
7
|
-
T = TypeVar("T")
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class RetryableException(Exception):
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
RETRIABLE_EXCEPTIONS: tuple[type[BaseException], ...] = (RetryableException,)
|
|
15
|
-
OVERLOADED_EXCEPTIONS: tuple[type[BaseException], ...] = ()
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def retry_db(func: Callable[P, T]) -> Callable[P, T]:
|
|
19
|
-
attempts = 3
|
|
20
|
-
|
|
21
|
-
@functools.wraps(func)
|
|
22
|
-
async def wrapper(*args, **kwargs):
|
|
23
|
-
for i in range(attempts):
|
|
24
|
-
if i == attempts - 1:
|
|
25
|
-
return await func(*args, **kwargs)
|
|
26
|
-
try:
|
|
27
|
-
return await func(*args, **kwargs)
|
|
28
|
-
except RETRIABLE_EXCEPTIONS:
|
|
29
|
-
await asyncio.sleep(0.01)
|
|
30
|
-
|
|
31
|
-
return wrapper
|
langgraph_storage/store.py
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import os
|
|
3
|
-
import threading
|
|
4
|
-
from collections import defaultdict
|
|
5
|
-
from collections.abc import Iterable
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
from langgraph.checkpoint.memory import PersistentDict
|
|
9
|
-
from langgraph.store.base import BaseStore, Op, Result
|
|
10
|
-
from langgraph.store.base.batch import AsyncBatchedBaseStore
|
|
11
|
-
from langgraph.store.memory import InMemoryStore
|
|
12
|
-
|
|
13
|
-
from langgraph_api.graph import resolve_embeddings
|
|
14
|
-
|
|
15
|
-
_STORE_CONFIG = None
|
|
16
|
-
DISABLE_FILE_PERSISTENCE = (
|
|
17
|
-
os.getenv("LANGGRAPH_DISABLE_FILE_PERSISTENCE", "false").lower() == "true"
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class DiskBackedInMemStore(InMemoryStore):
|
|
22
|
-
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
23
|
-
super().__init__(*args, **kwargs)
|
|
24
|
-
if not DISABLE_FILE_PERSISTENCE:
|
|
25
|
-
self._data = PersistentDict(dict, filename=_STORE_FILE)
|
|
26
|
-
self._vectors = PersistentDict(
|
|
27
|
-
lambda: defaultdict(dict), filename=_VECTOR_FILE
|
|
28
|
-
)
|
|
29
|
-
else:
|
|
30
|
-
self._data = InMemoryStore._data
|
|
31
|
-
self._vectors = InMemoryStore._vectors
|
|
32
|
-
self._load_data(self._data, which="data")
|
|
33
|
-
self._load_data(self._vectors, which="vectors")
|
|
34
|
-
|
|
35
|
-
def _load_data(self, container: PersistentDict, which: str) -> None:
|
|
36
|
-
if not container.filename:
|
|
37
|
-
return
|
|
38
|
-
try:
|
|
39
|
-
container.load()
|
|
40
|
-
except FileNotFoundError:
|
|
41
|
-
# It's okay if the file doesn't exist yet
|
|
42
|
-
pass
|
|
43
|
-
|
|
44
|
-
except (EOFError, ValueError) as e:
|
|
45
|
-
raise RuntimeError(
|
|
46
|
-
f"Failed to load store {which} from {container.filename}. "
|
|
47
|
-
"This may be due to changes in the stored data structure. "
|
|
48
|
-
"Consider clearing the local store by running: rm -rf .langgraph_api"
|
|
49
|
-
) from e
|
|
50
|
-
except Exception as e:
|
|
51
|
-
raise RuntimeError(
|
|
52
|
-
f"Unexpected error loading store {which} from {container.filename}: {str(e)}"
|
|
53
|
-
) from e
|
|
54
|
-
|
|
55
|
-
async def start_ttl_sweeper(self) -> asyncio.Task[None]:
|
|
56
|
-
return asyncio.create_task(asyncio.sleep(0))
|
|
57
|
-
|
|
58
|
-
def close(self) -> None:
|
|
59
|
-
self._data.close()
|
|
60
|
-
self._vectors.close()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class BatchedStore(AsyncBatchedBaseStore):
|
|
64
|
-
def __init__(self, store: BaseStore) -> None:
|
|
65
|
-
super().__init__()
|
|
66
|
-
self._store = store
|
|
67
|
-
|
|
68
|
-
def batch(self, ops: Iterable[Op]) -> list[Result]:
|
|
69
|
-
return self._store.batch(ops)
|
|
70
|
-
|
|
71
|
-
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
|
|
72
|
-
return await self._store.abatch(ops)
|
|
73
|
-
|
|
74
|
-
async def start_ttl_sweeper(self) -> asyncio.Task[None]:
|
|
75
|
-
return await self._store.start_ttl_sweeper()
|
|
76
|
-
|
|
77
|
-
def close(self) -> None:
|
|
78
|
-
self._store.close()
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
_STORE_FILE = os.path.join(".langgraph_api", "store.pckl")
|
|
82
|
-
_VECTOR_FILE = os.path.join(".langgraph_api", "store.vectors.pckl")
|
|
83
|
-
os.makedirs(".langgraph_api", exist_ok=True)
|
|
84
|
-
STORE = DiskBackedInMemStore()
|
|
85
|
-
BATCHED_STORE = threading.local()
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def set_store_config(config: dict) -> None:
|
|
89
|
-
global _STORE_CONFIG, STORE
|
|
90
|
-
_STORE_CONFIG = config.copy()
|
|
91
|
-
_STORE_CONFIG["index"]["embed"] = resolve_embeddings(_STORE_CONFIG.get("index", {}))
|
|
92
|
-
# Re-create the store
|
|
93
|
-
STORE.close()
|
|
94
|
-
STORE = DiskBackedInMemStore(index=_STORE_CONFIG.get("index", {}))
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def Store(*args: Any, **kwargs: Any) -> DiskBackedInMemStore:
|
|
98
|
-
if not hasattr(BATCHED_STORE, "store"):
|
|
99
|
-
BATCHED_STORE.store = BatchedStore(STORE)
|
|
100
|
-
return BATCHED_STORE.store
|
|
File without changes
|
|
File without changes
|
|
File without changes
|