From 4de8443c5c9cc0581025125e1bd4f26a7dba8dba Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Tue, 11 Mar 2025 20:09:12 -0700 Subject: [PATCH] Add default TTL in store & CLI --- .../langgraph/store/base/__init__.py | 72 +++++++++++++++++-- libs/checkpoint/langgraph/store/base/batch.py | 25 +++++-- libs/cli/langgraph_cli/config.py | 25 +++++++ libs/cli/schemas/schema.json | 34 ++++++++- libs/sdk-py/langgraph_sdk/client.py | 4 +- 5 files changed, 148 insertions(+), 12 deletions(-) diff --git a/libs/checkpoint/langgraph/store/base/__init__.py b/libs/checkpoint/langgraph/store/base/__init__.py index 190f61689..8dddab6d3 100644 --- a/libs/checkpoint/langgraph/store/base/__init__.py +++ b/libs/checkpoint/langgraph/store/base/__init__.py @@ -11,9 +11,19 @@ Core types: from abc import ABC, abstractmethod from datetime import datetime -from typing import Any, Iterable, Literal, NamedTuple, Optional, TypedDict, Union, cast +from typing import ( + Any, + Iterable, + Literal, + NamedTuple, + Optional, + TypedDict, + Union, + cast, +) from langchain_core.embeddings import Embeddings +from typing_extensions import override from langgraph.store.base.embed import ( AEmbeddingsFunc, @@ -24,6 +34,20 @@ from langgraph.store.base.embed import ( ) +class NotProvided: + """Sentinel singleton.""" + + def __bool__(self) -> Literal[False]: + return False + + @override + def __repr__(self) -> str: + return "NOT_GIVEN" + + +NOT_PROVIDED = NotProvided() + + class Item: """Represents a stored item with metadata. @@ -506,6 +530,13 @@ class TTLConfig(TypedDict, total=False): This can be overridden per-operation by explicitly setting refresh_ttl. Defaults to True if not configured. """ + default_ttl: Optional[float] + """Default TTL (time-to-live) in minutes for new items. + + If provided, new items will expire after this many minutes after their last access. + The expiration timer refreshes on both read and write operations. + Defaults to None (no expiration). + """ class IndexConfig(TypedDict, total=False): @@ -782,7 +813,7 @@ class BaseStore(ABC): value: dict[str, Any], index: Optional[Union[Literal[False], list[str]]] = None, *, - ttl: Optional[float] = None, + ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED, ) -> None: """Store or update an item in the store. @@ -840,7 +871,17 @@ class BaseStore(ABC): f"TTL is not supported by {self.__class__.__name__}. " f"Use a store implementation that supports TTL or set ttl=None." ) - self.batch([PutOp(namespace, str(key), value, index=index, ttl=ttl)]) + self.batch( + [ + PutOp( + namespace, + str(key), + value, + index=index, + ttl=_ensure_ttl(self.ttl_config, ttl), + ) + ] + ) def delete(self, namespace: tuple[str, ...], key: str) -> None: """Delete an item. @@ -1013,7 +1054,7 @@ class BaseStore(ABC): value: dict[str, Any], index: Optional[Union[Literal[False], list[str]]] = None, *, - ttl: Optional[float] = None, + ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED, ) -> None: """Asynchronously store or update an item in the store. @@ -1079,7 +1120,17 @@ class BaseStore(ABC): f"TTL is not supported by {self.__class__.__name__}. " f"Use a store implementation that supports TTL or set ttl=None." ) - await self.abatch([PutOp(namespace, str(key), value, index=index, ttl=ttl)]) + await self.abatch( + [ + PutOp( + namespace, + str(key), + value, + index=index, + ttl=_ensure_ttl(self.ttl_config, ttl), + ) + ] + ) async def adelete(self, namespace: tuple[str, ...], key: str) -> None: """Asynchronously delete an item. @@ -1178,6 +1229,17 @@ def _ensure_refresh( return True +def _ensure_ttl( + ttl_config: Optional[TTLConfig], + ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED, +) -> Optional[float]: + if ttl is NOT_PROVIDED: + if ttl_config: + return ttl_config.get("default_ttl") + return None + return ttl + + __all__ = [ "BaseStore", "Item", diff --git a/libs/checkpoint/langgraph/store/base/batch.py b/libs/checkpoint/langgraph/store/base/batch.py index 16d0b1f36..4883a3282 100644 --- a/libs/checkpoint/langgraph/store/base/batch.py +++ b/libs/checkpoint/langgraph/store/base/batch.py @@ -5,18 +5,21 @@ from collections.abc import Iterable from typing import Any, Callable, Literal, Optional, TypeVar, Union from langgraph.store.base import ( + NOT_PROVIDED, BaseStore, GetOp, Item, ListNamespacesOp, MatchCondition, NamespacePath, + NotProvided, Op, PutOp, Result, SearchItem, SearchOp, _ensure_refresh, + _ensure_ttl, _validate_namespace, ) @@ -121,12 +124,19 @@ class AsyncBatchedBaseStore(BaseStore): value: dict[str, Any], index: Optional[Union[Literal[False], list[str]]] = None, *, - ttl: Optional[float] = None, + ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED, ) -> None: assert not self._task.done() _validate_namespace(namespace) fut = self._loop.create_future() - self._aqueue.put_nowait((fut, PutOp(namespace, key, value, index, ttl=ttl))) + self._aqueue.put_nowait( + ( + fut, + PutOp( + namespace, key, value, index, ttl=_ensure_ttl(self.ttl_config, ttl) + ), + ) + ) return await fut async def adelete( @@ -213,11 +223,18 @@ class AsyncBatchedBaseStore(BaseStore): value: dict[str, Any], index: Optional[Union[Literal[False], list[str]]] = None, *, - ttl: Optional[float] = None, + ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED, ) -> None: _validate_namespace(namespace) asyncio.run_coroutine_threadsafe( - self.aput(namespace, key=key, value=value, index=index, ttl=ttl), self._loop + self.aput( + namespace, + key=key, + value=value, + index=index, + ttl=_ensure_ttl(self.ttl_config, ttl), + ), + self._loop, ).result() @_check_loop diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index 5082bb017..6200b1ad7 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -11,6 +11,24 @@ MIN_NODE_VERSION = "20" MIN_PYTHON_VERSION = "3.11" +class TTLConfig(TypedDict, total=False): + """Configuration for TTL (time-to-live) behavior in the store.""" + + refresh_on_read: bool + """Default behavior for refreshing TTLs on read operations (GET and SEARCH). + + If True, TTLs will be refreshed on read operations (get/search) by default. + This can be overridden per-operation by explicitly setting refresh_ttl. + Defaults to True if not configured. + """ + default_ttl: Optional[float] + """Optional. Default TTL (time-to-live) in minutes for new items. + + If provided, all new items will have this TTL unless explicitly overridden. + If omitted, items will have no TTL by default. + """ + + class IndexConfig(TypedDict, total=False): """Configuration for indexing documents for semantic search in the store. @@ -79,6 +97,13 @@ class StoreConfig(TypedDict, total=False): If omitted, no vector index is initialized. """ + ttl: Optional[TTLConfig] + """Optional. Defines the TTL (time-to-live) behavior configuration. + + If provided, the store will apply TTL settings according to the configuration. + If omitted, no TTL behavior is configured. + """ + class SecurityConfig(TypedDict, total=False): """Configuration for OpenAPI security definitions and requirements. diff --git a/libs/cli/schemas/schema.json b/libs/cli/schemas/schema.json index 37186cd95..65aa876e9 100644 --- a/libs/cli/schemas/schema.json +++ b/libs/cli/schemas/schema.json @@ -397,6 +397,17 @@ } ], "description": "Optional. Defines the vector-based semantic search configuration.\n\n- Generate embeddings according to `index.embed`\n- Enforce the embedding dimension given by `index.dims`\n- Embed only specified JSON fields (if any) from `index.fields`\n\nIf omitted, no vector index is initialized.\n" + }, + "ttl": { + "anyOf": [ + { + "$ref": "#/$defs/TTLConfig" + }, + { + "type": "null" + } + ], + "description": "Optional. Defines the TTL (time-to-live) behavior configuration.\n\nIf provided, the store will apply TTL settings according to the configuration.\nIf omitted, no TTL behavior is configured.\n" } }, "required": [] @@ -430,9 +441,30 @@ } }, "required": [] + }, + "TTLConfig": { + "title": "TTLConfig", + "description": "Configuration for TTL (time-to-live) behavior in the store.", + "type": "object", + "properties": { + "default_ttl": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "refresh_on_read": { + "type": "boolean" + } + }, + "required": [] } }, "title": "LangGraph CLI Configuration", "description": "Configuration schema for langgraph-cli", - "version": "v0" + "version": "v1" } \ No newline at end of file diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py index 16898646b..b1c9ba6c3 100644 --- a/libs/sdk-py/langgraph_sdk/client.py +++ b/libs/sdk-py/langgraph_sdk/client.py @@ -2163,7 +2163,7 @@ class StoreClient: "index": index, "ttl": ttl, } - await self.http.put("/store/items", json=payload) + await self.http.put("/store/items", json=_provided_vals(payload)) async def get_item( self, @@ -4307,7 +4307,7 @@ class SyncStoreClient: "index": index, "ttl": ttl, } - self.http.put("/store/items", json=payload) + self.http.put("/store/items", json=_provided_vals(payload)) def get_item( self,