langgraph-runtime-inmem 0.22.0__py3-none-any.whl → 0.23.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.
@@ -10,7 +10,7 @@ from langgraph_runtime_inmem import (
10
10
  store,
11
11
  )
12
12
 
13
- __version__ = "0.22.0"
13
+ __version__ = "0.23.0"
14
14
  __all__ = [
15
15
  "ops",
16
16
  "database",
@@ -0,0 +1,58 @@
1
+ """Periodic flushing for all PersistentDict stores."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import logging
7
+ import threading
8
+ import weakref
9
+
10
+ from langgraph.checkpoint.memory import PersistentDict
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ _stores: dict[str, weakref.ref[PersistentDict]] = {}
15
+ _flush_thread: tuple[threading.Event, threading.Thread] | None = None
16
+ _flush_interval: int = 10
17
+
18
+
19
+ def register_persistent_dict(d: PersistentDict) -> None:
20
+ """Register a PersistentDict for periodic flushing."""
21
+ global _flush_thread
22
+ _stores[d.filename] = weakref.ref(d)
23
+ if _flush_thread is None:
24
+ logger.info("Starting dev persistence flush loop")
25
+ stop_event = threading.Event()
26
+ _flush_thread = (
27
+ stop_event,
28
+ threading.Thread(
29
+ target=functools.partial(_flush_loop, stop_event), daemon=True
30
+ ),
31
+ )
32
+ _flush_thread[1].start()
33
+
34
+
35
+ def stop_flush_loop() -> None:
36
+ """Stop the background flush thread."""
37
+ global _flush_thread
38
+ if _flush_thread is not None:
39
+ logger.info("Stopping dev persistence flush loop")
40
+ _flush_thread[0].set()
41
+ _flush_thread[1].join()
42
+ _flush_thread = None
43
+
44
+
45
+ def _flush_loop(stop_event: threading.Event) -> None:
46
+ drop = set()
47
+ while not stop_event.wait(timeout=_flush_interval):
48
+ keys = list(_stores.keys())
49
+ for store_key in keys:
50
+ if store := _stores[store_key]():
51
+ store.sync()
52
+ else:
53
+ drop.add(store_key)
54
+ if drop:
55
+ for store_key in drop:
56
+ del _stores[store_key]
57
+ drop.clear()
58
+ logger.info("dev persistence flush loop exiting")
@@ -4,10 +4,21 @@ import logging
4
4
  import os
5
5
  import typing
6
6
  import uuid
7
- from collections.abc import AsyncIterator
7
+ from collections import defaultdict
8
+ from collections.abc import AsyncIterator, Callable
8
9
  from typing import Any
9
10
 
10
- from langgraph.checkpoint.memory import MemorySaver, PersistentDict
11
+ from langgraph.checkpoint.memory import (
12
+ InMemorySaver as InMemorySaverBase,
13
+ )
14
+ from langgraph.checkpoint.memory import (
15
+ PersistentDict,
16
+ )
17
+
18
+ from langgraph_runtime_inmem._persistence import (
19
+ register_persistent_dict,
20
+ stop_flush_loop,
21
+ )
11
22
 
12
23
  if typing.TYPE_CHECKING:
13
24
  from langchain_core.runnables import RunnableConfig
@@ -37,13 +48,15 @@ DISABLE_FILE_PERSISTENCE = (
37
48
  )
38
49
 
39
50
 
40
- class InMemorySaver(MemorySaver):
51
+ class InMemorySaver(InMemorySaverBase):
41
52
  def __init__(
42
53
  self,
43
54
  *,
44
55
  serde: SerializerProtocol | None = None,
56
+ __persistence_hook__: Callable[[PersistentDict], None] | None = None,
45
57
  ) -> None:
46
58
  self.filename = os.path.join(".langgraph_api", ".langgraph_checkpoint.")
59
+ self.latest_iter: AsyncIterator[CheckpointTuple] | None = None
47
60
  i = 0
48
61
 
49
62
  def factory(*args):
@@ -54,6 +67,8 @@ class InMemorySaver(MemorySaver):
54
67
  os.mkdir(".langgraph_api")
55
68
  thisfname = self.filename + str(i) + ".pckl"
56
69
  d = PersistentDict(*args, filename=thisfname)
70
+ if __persistence_hook__:
71
+ __persistence_hook__(d)
57
72
 
58
73
  try:
59
74
  d.load()
@@ -84,7 +99,7 @@ class InMemorySaver(MemorySaver):
84
99
 
85
100
  super().__init__(
86
101
  serde=serde if serde is not None else Serializer(),
87
- factory=factory if not DISABLE_FILE_PERSISTENCE else None,
102
+ factory=factory if not DISABLE_FILE_PERSISTENCE else defaultdict,
88
103
  )
89
104
 
90
105
  def put(
@@ -147,12 +162,10 @@ class InMemorySaver(MemorySaver):
147
162
 
148
163
  if not api_config.LANGGRAPH_ENCRYPTION:
149
164
  return data
150
- from langgraph_api.api.encryption_middleware import decrypt_json_if_needed
151
- from langgraph_api.encryption import get_encryption_instance
165
+ from langgraph_api.encryption import get_encryption
166
+ from langgraph_api.encryption.middleware import decrypt_json_if_needed
152
167
 
153
- result = await decrypt_json_if_needed(
154
- data, get_encryption_instance(), "checkpoint"
155
- )
168
+ result = await decrypt_json_if_needed(data, get_encryption(), "checkpoint")
156
169
  if result is None:
157
170
  raise ValueError("decrypt_json_if_needed returned None for non-None input")
158
171
  return result
@@ -199,6 +212,15 @@ class InMemorySaver(MemorySaver):
199
212
  pending_writes=tuple_.pending_writes,
200
213
  )
201
214
 
215
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
216
+ stop_flush_loop()
217
+ await super().__aexit__(exc_type, exc_val, exc_tb)
218
+
219
+ async def aget_iter(self, config: RunnableConfig) -> AsyncIterator[CheckpointTuple]:
220
+ tup = await self.aget_tuple(config)
221
+ if tup is not None:
222
+ yield tup
223
+
202
224
 
203
225
  MEMORY = None
204
226
 
@@ -206,12 +228,16 @@ MEMORY = None
206
228
  def Checkpointer(*args, unpack_hook=None, **kwargs):
207
229
  global MEMORY
208
230
  if MEMORY is None:
209
- MEMORY = InMemorySaver()
231
+ MEMORY = InMemorySaver(
232
+ __persistence_hook__=register_persistent_dict,
233
+ )
210
234
  if unpack_hook is not None:
211
235
  from langgraph_api.serde import Serializer
212
236
 
213
237
  saver = InMemorySaver(
214
- serde=Serializer(__unpack_ext_hook__=unpack_hook), **kwargs
238
+ serde=Serializer(__unpack_ext_hook__=unpack_hook),
239
+ __persistence_hook__=register_persistent_dict,
240
+ **kwargs,
215
241
  )
216
242
  saver.writes = MEMORY.writes
217
243
  saver.blobs = MEMORY.blobs
@@ -13,6 +13,7 @@ from langgraph.checkpoint.memory import PersistentDict
13
13
  from typing_extensions import TypedDict
14
14
 
15
15
  from langgraph_runtime_inmem import store
16
+ from langgraph_runtime_inmem._persistence import register_persistent_dict
16
17
  from langgraph_runtime_inmem.inmem_stream import start_stream, stop_stream
17
18
 
18
19
  if TYPE_CHECKING:
@@ -114,6 +115,10 @@ class InMemoryRetryCounter:
114
115
  GLOBAL_RETRY_COUNTER = InMemoryRetryCounter()
115
116
  GLOBAL_STORE = GlobalStore(filename=OPS_FILENAME)
116
117
 
118
+ # Register for periodic flushing
119
+ register_persistent_dict(GLOBAL_STORE)
120
+ register_persistent_dict(GLOBAL_RETRY_COUNTER._counters)
121
+
117
122
 
118
123
  class InMemConnectionProto:
119
124
  def __init__(self):
@@ -30,6 +30,9 @@ async def lifespan(
30
30
  ):
31
31
  import langgraph_api.config as config
32
32
  from langgraph_api import __version__, feature_flags, graph, thread_ttl
33
+ from langgraph_api import (
34
+ _checkpointer as api_checkpointer,
35
+ )
33
36
  from langgraph_api import store as api_store
34
37
  from langgraph_api.asyncio import SimpleTaskGroup, set_event_loop
35
38
  from langgraph_api.http import start_http_client, stop_http_client
@@ -54,6 +57,7 @@ async def lifespan(
54
57
 
55
58
  await start_http_client()
56
59
  await start_pool()
60
+ await api_checkpointer.start_checkpointer()
57
61
  await start_ui_bundler()
58
62
 
59
63
  async def _log_graph_load_failure(err: graph.GraphLoadError) -> None:
@@ -119,6 +123,7 @@ async def lifespan(
119
123
  pass
120
124
  finally:
121
125
  await api_store.exit_store()
126
+ await api_checkpointer.exit_checkpointer()
122
127
  await stop_ui_bundler()
123
128
  await graph.stop_remote_graphs()
124
129
  await stop_http_client()
@@ -10,7 +10,7 @@ import typing
10
10
  import uuid
11
11
  from collections import defaultdict
12
12
  from collections.abc import AsyncIterator, Sequence
13
- from contextlib import asynccontextmanager
13
+ from contextlib import AsyncExitStack, asynccontextmanager
14
14
  from datetime import UTC, datetime, timedelta
15
15
  from typing import Any, Literal, cast
16
16
  from uuid import UUID, uuid4
@@ -150,6 +150,8 @@ class Assistants(Authenticated):
150
150
  select: list[AssistantSelectField] | None = None,
151
151
  ctx: Auth.types.BaseAuthContext | None = None,
152
152
  ) -> tuple[AsyncIterator[Assistant], int]:
153
+ from langgraph_api.graph import assert_graph_exists
154
+
153
155
  metadata = metadata if metadata is not None else {}
154
156
  filters = await Assistants.handle_event(
155
157
  ctx,
@@ -159,6 +161,9 @@ class Assistants(Authenticated):
159
161
  ),
160
162
  )
161
163
 
164
+ if graph_id is not None:
165
+ assert_graph_exists(graph_id)
166
+
162
167
  # Get all assistants and filter them
163
168
  assistants = conn.store["assistants"]
164
169
  filtered_assistants = [
@@ -250,7 +255,7 @@ class Assistants(Authenticated):
250
255
  description: str | None = None,
251
256
  ) -> AsyncIterator[Assistant]:
252
257
  """Insert an assistant."""
253
- from langgraph_api.graph import GRAPHS
258
+ from langgraph_api.graph import assert_graph_exists
254
259
 
255
260
  assistant_id = _ensure_uuid(assistant_id)
256
261
  metadata = metadata if metadata is not None else {}
@@ -273,8 +278,7 @@ class Assistants(Authenticated):
273
278
  detail="Cannot specify both configurable and context. Prefer setting context alone. Context was introduced in LangGraph 0.6.0 and is the long term planned replacement for configurable.",
274
279
  )
275
280
 
276
- if graph_id not in GRAPHS:
277
- raise HTTPException(status_code=404, detail=f"Graph {graph_id} not found")
281
+ assert_graph_exists(graph_id)
278
282
 
279
283
  # Keep config and context up to date with one another
280
284
  if config.get("configurable"):
@@ -365,6 +369,8 @@ class Assistants(Authenticated):
365
369
  Returns:
366
370
  return the updated assistant model.
367
371
  """
372
+ from langgraph_api.graph import assert_graph_exists
373
+
368
374
  assistant_id = _ensure_uuid(assistant_id)
369
375
  metadata = metadata if metadata is not None else {}
370
376
  config = config if config is not None else {}
@@ -387,6 +393,9 @@ class Assistants(Authenticated):
387
393
  detail="Cannot specify both configurable and context. Prefer setting context alone. Context was introduced in LangGraph 0.6.0 and is the long term planned replacement for configurable.",
388
394
  )
389
395
 
396
+ if graph_id is not None:
397
+ assert_graph_exists(graph_id)
398
+
390
399
  # Keep config and context up to date with one another
391
400
  if config.get("configurable"):
392
401
  context = config["configurable"]
@@ -462,55 +471,85 @@ class Assistants(Authenticated):
462
471
 
463
472
  @staticmethod
464
473
  async def delete(
465
- conn: InMemConnectionProto,
474
+ conn: InMemConnectionProto | None,
466
475
  assistant_id: UUID,
467
476
  ctx: Auth.types.BaseAuthContext | None = None,
477
+ *,
478
+ delete_threads: bool = False,
468
479
  ) -> AsyncIterator[UUID]:
469
480
  """Delete an assistant by ID."""
470
- assistant_id = _ensure_uuid(assistant_id)
471
- filters = await Assistants.handle_event(
472
- ctx,
473
- "delete",
474
- Auth.types.AssistantsDelete(
475
- assistant_id=assistant_id,
476
- ),
477
- )
478
- assistant = next(
479
- (a for a in conn.store["assistants"] if a["assistant_id"] == assistant_id),
480
- None,
481
- )
481
+ async with AsyncExitStack() as stack:
482
+ if conn is None:
483
+ conn = await stack.enter_async_context(connect())
482
484
 
483
- if not assistant:
484
- raise HTTPException(
485
- status_code=404, detail=f"Assistant with ID {assistant_id} not found"
485
+ assistant_id = _ensure_uuid(assistant_id)
486
+ filters = await Assistants.handle_event(
487
+ ctx,
488
+ "delete",
489
+ Auth.types.AssistantsDelete(
490
+ assistant_id=assistant_id,
491
+ ),
486
492
  )
487
- elif filters and not _check_filter_match(assistant["metadata"], filters):
488
- raise HTTPException(
489
- status_code=404, detail=f"Assistant with ID {assistant_id} not found"
493
+ assistant = next(
494
+ (
495
+ a
496
+ for a in conn.store["assistants"]
497
+ if a["assistant_id"] == assistant_id
498
+ ),
499
+ None,
490
500
  )
491
501
 
492
- # Cancel all in-flight runs for this assistant before deletion
493
- await Runs.cancel(
494
- conn,
495
- assistant_id=assistant_id,
496
- action="interrupt",
497
- ctx=ctx,
498
- )
502
+ if not assistant:
503
+ raise HTTPException(
504
+ status_code=404,
505
+ detail=f"Assistant with ID {assistant_id} not found",
506
+ )
507
+ elif filters and not _check_filter_match(assistant["metadata"], filters):
508
+ raise HTTPException(
509
+ status_code=404,
510
+ detail=f"Assistant with ID {assistant_id} not found",
511
+ )
499
512
 
500
- conn.store["assistants"] = [
501
- a for a in conn.store["assistants"] if a["assistant_id"] != assistant_id
502
- ]
503
- # Cascade delete assistant versions
504
- conn.store["assistant_versions"] = [
505
- v
506
- for v in conn.store["assistant_versions"]
507
- if v["assistant_id"] != assistant_id
508
- ]
513
+ if delete_threads:
514
+ threads_to_delete = [
515
+ t["thread_id"]
516
+ for t in conn.store["threads"]
517
+ if t.get("metadata", {}).get("assistant_id") == str(assistant_id)
518
+ ]
519
+ for thread_id in threads_to_delete:
520
+ try:
521
+ async for _ in await Threads.delete(conn, thread_id, ctx=ctx):
522
+ pass
523
+ except HTTPException:
524
+ await logger.awarning(
525
+ "Skipping thread deletion during cascade delete (user lacks permission)",
526
+ thread_id=thread_id,
527
+ assistant_id=assistant_id,
528
+ )
509
529
 
510
- async def _yield_deleted():
511
- yield assistant_id
530
+ # 3. Cancel in-flight runs AFTER auth validation
531
+ await Runs.cancel(
532
+ conn,
533
+ assistant_id=assistant_id,
534
+ action="interrupt",
535
+ ctx=ctx,
536
+ )
512
537
 
513
- return _yield_deleted()
538
+ # 4. Delete assistant
539
+ conn.store["assistants"] = [
540
+ a for a in conn.store["assistants"] if a["assistant_id"] != assistant_id
541
+ ]
542
+ # Cascade delete assistant versions
543
+ conn.store["assistant_versions"] = [
544
+ v
545
+ for v in conn.store["assistant_versions"]
546
+ if v["assistant_id"] != assistant_id
547
+ ]
548
+
549
+ async def _yield_deleted():
550
+ yield assistant_id
551
+
552
+ return _yield_deleted()
514
553
 
515
554
  @staticmethod
516
555
  async def set_latest(
@@ -632,6 +671,8 @@ class Assistants(Authenticated):
632
671
  ctx: Auth.types.BaseAuthContext | None = None,
633
672
  ) -> int:
634
673
  """Get count of assistants."""
674
+ from langgraph_api.graph import assert_graph_exists
675
+
635
676
  metadata = metadata if metadata is not None else {}
636
677
  filters = await Assistants.handle_event(
637
678
  ctx,
@@ -641,6 +682,9 @@ class Assistants(Authenticated):
641
682
  ),
642
683
  )
643
684
 
685
+ if graph_id is not None:
686
+ assert_graph_exists(graph_id)
687
+
644
688
  count = 0
645
689
  for assistant in conn.store["assistants"]:
646
690
  if (
@@ -10,6 +10,8 @@ from langgraph.store.base import BaseStore, Op, Result
10
10
  from langgraph.store.base.batch import AsyncBatchedBaseStore
11
11
  from langgraph.store.memory import InMemoryStore
12
12
 
13
+ from langgraph_runtime_inmem._persistence import register_persistent_dict
14
+
13
15
  _STORE_CONFIG = None
14
16
  DISABLE_FILE_PERSISTENCE = (
15
17
  os.getenv("LANGGRAPH_DISABLE_FILE_PERSISTENCE", "false").lower() == "true"
@@ -24,6 +26,8 @@ class DiskBackedInMemStore(InMemoryStore):
24
26
  self._vectors = PersistentDict(
25
27
  lambda: defaultdict(dict), filename=_VECTOR_FILE
26
28
  )
29
+ register_persistent_dict(self._data)
30
+ register_persistent_dict(self._vectors)
27
31
  else:
28
32
  self._data = InMemoryStore._data
29
33
  self._vectors = InMemoryStore._vectors
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-runtime-inmem
3
- Version: 0.22.0
3
+ Version: 0.23.0
4
4
  Summary: Inmem implementation for the LangGraph API server.
5
5
  Author-email: Will Fu-Hinthorn <will@langchain.dev>
6
6
  License: Elastic-2.0
@@ -0,0 +1,15 @@
1
+ langgraph_runtime_inmem/__init__.py,sha256=oKfMmvvPRewTlo9oduB3tCC3l2b2MiD_9lka4PMm_5M,337
2
+ langgraph_runtime_inmem/_persistence.py,sha256=exchMr_NQB_h7PHt0vq5QBh25cOGoW0jHAFo07b1BFI,1711
3
+ langgraph_runtime_inmem/checkpoint.py,sha256=VD5c6CktsToo_f4qPe1WP_csdonQoOb7h5lHv4U0ZAE,8372
4
+ langgraph_runtime_inmem/database.py,sha256=iP7W1SI4kUkqcHtkg3aMmP-YLgZfvMHANwN-P4Pb1pY,6607
5
+ langgraph_runtime_inmem/inmem_stream.py,sha256=PFLWbsxU8RqbT5mYJgNk6v5q6TWJRIY1hkZWhJF8nkI,9094
6
+ langgraph_runtime_inmem/lifespan.py,sha256=51w3ZKvxcosd7XKkTE2Tnxtr3tux4rJuChGEN0CuvCY,4935
7
+ langgraph_runtime_inmem/metrics.py,sha256=_YiSkLnhQvHpMktk38SZo0abyL-5GihfVAtBo0-lFIc,403
8
+ langgraph_runtime_inmem/ops.py,sha256=OZ1VicFoh9eOxD15LbZsmCZ1JTaWkMUjdY0nF0t9e-k,120806
9
+ langgraph_runtime_inmem/queue.py,sha256=WM6ZJu25QPVjFXeJYW06GALLUgRsnRrA4YdypR0oG0U,9584
10
+ langgraph_runtime_inmem/retry.py,sha256=XmldOP4e_H5s264CagJRVnQMDFcEJR_dldVR1Hm5XvM,763
11
+ langgraph_runtime_inmem/routes.py,sha256=VVNxgJ8FWI3kDBoIgQUWN1gY5ivo7L954Agxzv72TAY,1377
12
+ langgraph_runtime_inmem/store.py,sha256=a3YKsLnFv4bu3zPvagIFv0xmtrIp_pmGvj1CnD3PHL0,3682
13
+ langgraph_runtime_inmem-0.23.0.dist-info/METADATA,sha256=9x1G8RrISXklr1MikxsE4kJFZTswjmATqNgA0EsPQYY,570
14
+ langgraph_runtime_inmem-0.23.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
15
+ langgraph_runtime_inmem-0.23.0.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- langgraph_runtime_inmem/__init__.py,sha256=MEt3B-cz2OdptjyXn7hgqdFgWBMBLaSY3wsKSEeB8Cg,337
2
- langgraph_runtime_inmem/checkpoint.py,sha256=lFUjSGra3xqD39FY3U-PgxYMXU6j1L28WPajmXT7-YE,7478
3
- langgraph_runtime_inmem/database.py,sha256=g2XYa5KN-T8MbDeFH9sfUApDG62Wp4BACumVnDtxYhI,6403
4
- langgraph_runtime_inmem/inmem_stream.py,sha256=PFLWbsxU8RqbT5mYJgNk6v5q6TWJRIY1hkZWhJF8nkI,9094
5
- langgraph_runtime_inmem/lifespan.py,sha256=fCoYcN_h0cxmj6-muC-f0csPdSpyepZuGRD1yBrq4XM,4755
6
- langgraph_runtime_inmem/metrics.py,sha256=_YiSkLnhQvHpMktk38SZo0abyL-5GihfVAtBo0-lFIc,403
7
- langgraph_runtime_inmem/ops.py,sha256=qJaQb138aL9D8ROy4yRaxlcaCuiNfoSDIX7m6lCWndE,119178
8
- langgraph_runtime_inmem/queue.py,sha256=WM6ZJu25QPVjFXeJYW06GALLUgRsnRrA4YdypR0oG0U,9584
9
- langgraph_runtime_inmem/retry.py,sha256=XmldOP4e_H5s264CagJRVnQMDFcEJR_dldVR1Hm5XvM,763
10
- langgraph_runtime_inmem/routes.py,sha256=VVNxgJ8FWI3kDBoIgQUWN1gY5ivo7L954Agxzv72TAY,1377
11
- langgraph_runtime_inmem/store.py,sha256=rTfL1JJvd-j4xjTrL8qDcynaWF6gUJ9-GDVwH0NBD_I,3506
12
- langgraph_runtime_inmem-0.22.0.dist-info/METADATA,sha256=viwco9HtZ2XtqNpn1nnBoLT2r2_8fvT8tqO3s2LwjHo,570
13
- langgraph_runtime_inmem-0.22.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
- langgraph_runtime_inmem-0.22.0.dist-info/RECORD,,