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.

@@ -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
@@ -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
@@ -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