-
Notifications
You must be signed in to change notification settings - Fork 33
feat(agent-memory): add create_checkpointer() factory with TTL support for LangGraph agents #221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
santosh-nallur
wants to merge
21
commits into
SAP:main
Choose a base branch
from
santosh-nallur:feat/local-factory
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
06981f8
feat(agent-memory): add create_checkpointer() factory for LangGraph a…
santosh-nallur fb16425
test(agent-memory): add unit tests for create_checkpointer() factory
santosh-nallur 563583f
docs(agent-memory): document LangGraph checkpointer factory in user g…
santosh-nallur 03e957d
Remove unnecessary ticket reference from test docstring
santosh-nallur e9b83b0
Add logger for in-memory checkpointer usage
santosh-nallur 36213a7
fix(agent-memory): upgrade InMemorySaver log from info to warning in …
santosh-nallur 28af397
feat(agent-memory): add TimedInMemorySaver with TTL and update factory
santosh-nallur ed6d8c4
docs(agent-memory): document ttl_seconds and TimedInMemorySaver in us…
santosh-nallur 7ec8c83
refactor(agent-memory): remove TimedInMemorySaver, retain ttl_seconds…
santosh-nallur e7fb5d6
docs(agent-memory): remove premature @agent_config TTL example
santosh-nallur 6664b36
feat(agent-memory): restore TimedInMemorySaver with TTL support
santosh-nallur 3306006
docs(agent-memory): update user guide for TimedInMemorySaver with TTL
santosh-nallur 2df4371
Apply review comment
santosh-nallur 19ba72c
feat(agent-memory): add langgraph optional dependency and update prer…
santosh-nallur 0c2bbbc
fix(agent-memory): bump langgraph optional dep lower bound to >=1.0.0
santosh-nallur 9a553cb
docs(agent-memory): add @agent_config example for TTL configuration
santosh-nallur 08f01fa
docs(agent-memory): update thread_ttl_seconds comment to clarify dura…
santosh-nallur cc896e8
docs (agent-memory): update user-guide.md
santosh-nallur ed689ea
revert pre-commit dependency
santosh-nallur e6e125a
feat(agent-memory): update langgraph installation instruction
santosh-nallur 1673f0f
feat(agent-memory): update langgraph installation instruction
santosh-nallur File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| """Factory subpackage for SAP Agent Memory framework adapters. | ||
| Each module in this package provides a factory function that creates the | ||
| appropriate framework-specific implementation for the current environment. | ||
| Available factories: | ||
| - :func:`sap_cloud_sdk.agent_memory.factory.langgraph_checkpoint.create_checkpointer` | ||
| — returns a LangGraph ``BaseCheckpointSaver`` (short-term memory) | ||
| Usage: | ||
| from sap_cloud_sdk.agent_memory.factory.langgraph_checkpoint import create_checkpointer | ||
| """ |
111 changes: 111 additions & 0 deletions
111
src/sap_cloud_sdk/agent_memory/factory/_timed_memory.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| """TimedInMemorySaver — InMemorySaver with background TTL-based thread eviction. | ||
|
|
||
| Not intended for direct use. Use ``create_checkpointer(ttl_seconds=...)`` instead. | ||
| """ | ||
|
|
||
| import logging | ||
| import threading | ||
| import time | ||
| from typing import Any | ||
|
|
||
| from langchain_core.runnables import RunnableConfig | ||
| from langgraph.checkpoint.base import ChannelVersions, Checkpoint, CheckpointMetadata | ||
| from langgraph.checkpoint.memory import InMemorySaver | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| _SWEEP_INTERVAL_SECONDS = 60 | ||
|
|
||
|
|
||
| class TimedInMemorySaver(InMemorySaver): | ||
| """InMemorySaver with background TTL-based thread eviction. | ||
|
|
||
| Tracks last-active time per thread and evicts inactive threads via a | ||
| daemon background sweep. The sweep is fully decoupled from the read/write | ||
| path — ``put()`` only updates the activity timestamp. | ||
|
|
||
| This approximates server-side TTL semantics for in-process storage: | ||
| - ``put()`` records last-active timestamp (hot path stays clean) | ||
| - A daemon sweep thread evicts inactive threads every 60 seconds | ||
| - Eviction is best-effort — a thread may live up to | ||
| ``ttl_seconds + 60`` seconds before deletion | ||
| - State does not survive process restarts (inherent to InMemorySaver) | ||
|
|
||
| Args: | ||
| ttl_seconds: Inactivity threshold in seconds. Threads inactive for | ||
| longer than this are evicted. Default: 3600 (1 hour). | ||
|
|
||
| Example:: | ||
|
|
||
| from sap_cloud_sdk.agent_memory.factory.langgraph_checkpoint import ( | ||
| create_checkpointer, | ||
| ) | ||
|
|
||
| checkpointer = create_checkpointer(ttl_seconds=3600) | ||
| """ | ||
|
|
||
| def __init__(self, *, ttl_seconds: int = 3600, **kwargs: Any) -> None: | ||
| super().__init__(**kwargs) | ||
| self.ttl_seconds = ttl_seconds | ||
| self._last_active: dict[str, float] = {} | ||
| self._lock = threading.Lock() | ||
| self._sweeper = threading.Thread( | ||
| target=self._sweep_loop, | ||
| daemon=True, | ||
| name="TimedInMemorySaver-sweeper", | ||
| ) | ||
| self._sweeper.start() | ||
| logger.debug( | ||
| "TimedInMemorySaver started with ttl_seconds=%d, sweep_interval=%ds", | ||
| ttl_seconds, | ||
| _SWEEP_INTERVAL_SECONDS, | ||
| ) | ||
|
|
||
| def put( | ||
| self, | ||
| config: RunnableConfig, | ||
| checkpoint: Checkpoint, | ||
| metadata: CheckpointMetadata, | ||
| new_versions: ChannelVersions, | ||
| ) -> RunnableConfig: | ||
| """Save checkpoint and refresh thread activity timestamp.""" | ||
| thread_id: str = config["configurable"]["thread_id"] | ||
| with self._lock: | ||
| self._last_active[thread_id] = time.monotonic() | ||
| return super().put(config, checkpoint, metadata, new_versions) | ||
|
|
||
| async def aput( | ||
| self, | ||
| config: RunnableConfig, | ||
| checkpoint: Checkpoint, | ||
| metadata: CheckpointMetadata, | ||
| new_versions: ChannelVersions, | ||
| ) -> RunnableConfig: | ||
| """Async version of put — refreshes thread activity timestamp.""" | ||
| thread_id: str = config["configurable"]["thread_id"] | ||
| with self._lock: | ||
| self._last_active[thread_id] = time.monotonic() | ||
| return await super().aput(config, checkpoint, metadata, new_versions) | ||
|
|
||
| def _sweep_loop(self) -> None: | ||
| while True: | ||
| time.sleep(_SWEEP_INTERVAL_SECONDS) | ||
| self._evict_expired() | ||
|
|
||
| def _evict_expired(self) -> None: | ||
| now = time.monotonic() | ||
| with self._lock: | ||
| expired = [ | ||
| tid | ||
| for tid, ts in self._last_active.items() | ||
| if now - ts > self.ttl_seconds | ||
| ] | ||
| for tid in expired: | ||
| self.delete_thread(tid) | ||
| with self._lock: | ||
| self._last_active.pop(tid, None) | ||
| logger.info( | ||
| "TimedInMemorySaver: evicted inactive thread '%s' (inactive for >%ds)", | ||
| tid, | ||
| self.ttl_seconds, | ||
| ) |
75 changes: 75 additions & 0 deletions
75
src/sap_cloud_sdk/agent_memory/factory/langgraph_checkpoint.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| """LangGraph checkpointer factory for SAP Agent Memory. | ||
|
|
||
| Usage:: | ||
|
|
||
| from sap_cloud_sdk.agent_memory.factory.langgraph_checkpoint import create_checkpointer | ||
|
|
||
| # No TTL — plain InMemorySaver | ||
| checkpointer = create_checkpointer() | ||
|
|
||
| # With TTL — TimedInMemorySaver evicts inactive threads automatically | ||
| checkpointer = create_checkpointer(ttl_seconds=3600) | ||
|
|
||
| app = workflow.compile(checkpointer=checkpointer) | ||
|
|
||
| # Or with LangChain create_agent: | ||
| from langchain.agents import create_agent | ||
| agent = create_agent(model="...", tools=[...], checkpointer=checkpointer) | ||
| """ | ||
|
|
||
| import logging | ||
| from typing import Optional | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def create_checkpointer(*, ttl_seconds: Optional[int] = None): | ||
| """Create a LangGraph checkpointer for the current environment. | ||
|
|
||
| Returns LangGraph's ``InMemorySaver`` (no TTL) or ``TimedInMemorySaver`` | ||
| (with TTL-based thread eviction). State is held in-process and does not | ||
| survive restarts. | ||
|
|
||
| Args: | ||
| ttl_seconds: Evict threads inactive for this many seconds. | ||
| ``None`` (default) disables eviction. | ||
|
|
||
| Returns: | ||
| BaseCheckpointSaver instance. | ||
|
|
||
| Raises: | ||
| ImportError: If langgraph is not installed. | ||
|
|
||
| Example — no TTL:: | ||
|
|
||
| checkpointer = create_checkpointer() | ||
| app = workflow.compile(checkpointer=checkpointer) | ||
|
|
||
| Example — evict threads inactive for 1 hour:: | ||
|
|
||
| checkpointer = create_checkpointer(ttl_seconds=3600) | ||
| app = workflow.compile(checkpointer=checkpointer) | ||
| """ | ||
| try: | ||
| from langgraph.checkpoint.memory import InMemorySaver | ||
| except ImportError: | ||
| raise ImportError( | ||
| "langgraph is required for create_checkpointer(). " | ||
| "Install it with: pip install sap-cloud-sdk[langgraph] or pip install langgraph" | ||
| ) | ||
|
|
||
| if ttl_seconds is not None: | ||
| from sap_cloud_sdk.agent_memory.factory._timed_memory import TimedInMemorySaver | ||
|
|
||
| logger.warning( | ||
| "create_checkpointer(): using TimedInMemorySaver(ttl_seconds=%d) — " | ||
| "session state is in-process only and will be lost on process exit.", | ||
| ttl_seconds, | ||
| ) | ||
| return TimedInMemorySaver(ttl_seconds=ttl_seconds) | ||
|
|
||
| logger.warning( | ||
| "create_checkpointer(): using InMemorySaver — " | ||
| "session state is in-process only and will be lost on process exit." | ||
| ) | ||
| return InMemorySaver() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cassiofariasmachado do we need to make anything to install it conditionally to langchain install?