langgraph-api 0.0.26__py3-none-any.whl → 0.0.28__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.

Files changed (53) hide show
  1. langgraph_api/api/__init__.py +2 -0
  2. langgraph_api/api/assistants.py +43 -13
  3. langgraph_api/api/meta.py +2 -1
  4. langgraph_api/api/runs.py +14 -1
  5. langgraph_api/api/ui.py +68 -0
  6. langgraph_api/asyncio.py +43 -4
  7. langgraph_api/auth/middleware.py +2 -2
  8. langgraph_api/cli.py +72 -57
  9. langgraph_api/config.py +23 -1
  10. langgraph_api/cron_scheduler.py +1 -1
  11. langgraph_api/graph.py +5 -0
  12. langgraph_api/http.py +24 -7
  13. langgraph_api/js/.gitignore +2 -0
  14. langgraph_api/js/build.mts +49 -3
  15. langgraph_api/js/client.mts +84 -40
  16. langgraph_api/js/global.d.ts +1 -0
  17. langgraph_api/js/package.json +15 -7
  18. langgraph_api/js/remote.py +662 -16
  19. langgraph_api/js/src/graph.mts +5 -4
  20. langgraph_api/js/sse.py +138 -0
  21. langgraph_api/js/tests/api.test.mts +28 -0
  22. langgraph_api/js/tests/compose-postgres.yml +2 -2
  23. langgraph_api/js/tests/graphs/agent.css +1 -0
  24. langgraph_api/js/tests/graphs/agent.ui.tsx +10 -0
  25. langgraph_api/js/tests/graphs/package.json +2 -2
  26. langgraph_api/js/tests/graphs/yarn.lock +13 -13
  27. langgraph_api/js/yarn.lock +710 -1187
  28. langgraph_api/lifespan.py +15 -5
  29. langgraph_api/logging.py +9 -0
  30. langgraph_api/metadata.py +5 -1
  31. langgraph_api/middleware/http_logger.py +1 -1
  32. langgraph_api/patch.py +2 -0
  33. langgraph_api/queue_entrypoint.py +63 -0
  34. langgraph_api/schema.py +2 -0
  35. langgraph_api/stream.py +1 -0
  36. langgraph_api/webhook.py +42 -0
  37. langgraph_api/{queue.py → worker.py} +52 -166
  38. {langgraph_api-0.0.26.dist-info → langgraph_api-0.0.28.dist-info}/METADATA +8 -8
  39. {langgraph_api-0.0.26.dist-info → langgraph_api-0.0.28.dist-info}/RECORD +49 -46
  40. langgraph_storage/database.py +8 -22
  41. langgraph_storage/inmem_stream.py +108 -0
  42. langgraph_storage/ops.py +80 -57
  43. langgraph_storage/queue.py +126 -103
  44. langgraph_storage/retry.py +5 -1
  45. langgraph_storage/store.py +5 -1
  46. openapi.json +3 -3
  47. langgraph_api/js/client.new.mts +0 -861
  48. langgraph_api/js/remote_new.py +0 -694
  49. langgraph_api/js/remote_old.py +0 -667
  50. langgraph_api/js/server_sent_events.py +0 -126
  51. {langgraph_api-0.0.26.dist-info → langgraph_api-0.0.28.dist-info}/LICENSE +0 -0
  52. {langgraph_api-0.0.26.dist-info → langgraph_api-0.0.28.dist-info}/WHEEL +0 -0
  53. {langgraph_api-0.0.26.dist-info → langgraph_api-0.0.28.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(app: Starlette):
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
- tg.create_task(queue(config.N_JOBS_PER_WORKER, config.BG_JOB_TIMEOUT_SECS))
35
- if config.FF_CRONS_ENABLED and plus_features_enabled():
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.warning("Failed to submit metadata", exc_info=e)
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
@@ -71,6 +71,8 @@ class Assistant(TypedDict):
71
71
  """The ID of the assistant."""
72
72
  graph_id: str
73
73
  """The ID of the graph."""
74
+ name: str
75
+ """The name of the assistant."""
74
76
  config: Config
75
77
  """The assistant config."""
76
78
  created_at: datetime
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")
@@ -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 BG_JOB_HEARTBEAT, STATS_INTERVAL_SECS
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
- if user is not None:
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
- pass
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"].pop("webhook", None)
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 > MAX_RETRY_ATTEMPTS:
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), timeout)
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
- await Threads.set_status(conn, run["thread_id"], checkpoint, exception)
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.26
3
+ Version: 0.0.28
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.27.0)
15
- Requires-Dist: jsonschema-rs (>=0.25.0,<0.26.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.3.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.10.1)
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.4.0,<25.0.0)
26
- Requires-Dist: tenacity (>=8.3.0,<10)
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 sesrver is backed by a predominently in-memory data store that is persisted to local disk when the server is restarted.
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