memory-reuse 0.1.0__tar.gz
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.
- memory_reuse-0.1.0/.gitignore +51 -0
- memory_reuse-0.1.0/CHANGELOG.md +44 -0
- memory_reuse-0.1.0/LICENSE +21 -0
- memory_reuse-0.1.0/PKG-INFO +276 -0
- memory_reuse-0.1.0/README.md +234 -0
- memory_reuse-0.1.0/examples/basic_exact_cache.py +67 -0
- memory_reuse-0.1.0/examples/langgraph_agent_example.py +107 -0
- memory_reuse-0.1.0/examples/langgraph_math_agent.py +433 -0
- memory_reuse-0.1.0/memory_reuse/__init__.py +44 -0
- memory_reuse-0.1.0/memory_reuse/_utils.py +125 -0
- memory_reuse-0.1.0/memory_reuse/backends/__init__.py +21 -0
- memory_reuse-0.1.0/memory_reuse/backends/base.py +76 -0
- memory_reuse-0.1.0/memory_reuse/backends/memory.py +170 -0
- memory_reuse-0.1.0/memory_reuse/backends/redis.py +217 -0
- memory_reuse-0.1.0/memory_reuse/cache/__init__.py +12 -0
- memory_reuse-0.1.0/memory_reuse/cache/exact.py +194 -0
- memory_reuse-0.1.0/memory_reuse/cache/tool.py +170 -0
- memory_reuse-0.1.0/memory_reuse/config.py +99 -0
- memory_reuse-0.1.0/memory_reuse/core.py +224 -0
- memory_reuse-0.1.0/memory_reuse/exceptions.py +42 -0
- memory_reuse-0.1.0/memory_reuse/integrations/__init__.py +26 -0
- memory_reuse-0.1.0/memory_reuse/integrations/langgraph.py +235 -0
- memory_reuse-0.1.0/memory_reuse/integrations/litellm.py +277 -0
- memory_reuse-0.1.0/memory_reuse/py.typed +0 -0
- memory_reuse-0.1.0/memory_reuse/stats.py +112 -0
- memory_reuse-0.1.0/pyproject.toml +109 -0
- memory_reuse-0.1.0/tests/__init__.py +0 -0
- memory_reuse-0.1.0/tests/conftest.py +48 -0
- memory_reuse-0.1.0/tests/integration/__init__.py +0 -0
- memory_reuse-0.1.0/tests/integration/test_langgraph_integration.py +220 -0
- memory_reuse-0.1.0/tests/unit/__init__.py +0 -0
- memory_reuse-0.1.0/tests/unit/test_backends.py +123 -0
- memory_reuse-0.1.0/tests/unit/test_exact_cache.py +108 -0
- memory_reuse-0.1.0/tests/unit/test_litellm_integration.py +338 -0
- memory_reuse-0.1.0/tests/unit/test_stats.py +100 -0
- memory_reuse-0.1.0/tests/unit/test_tool_cache.py +103 -0
- memory_reuse-0.1.0/tests/unit/test_utils.py +109 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.pyo
|
|
6
|
+
*.pyd
|
|
7
|
+
|
|
8
|
+
# Virtual environments
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
env/
|
|
12
|
+
ENV/
|
|
13
|
+
|
|
14
|
+
# Distribution / packaging
|
|
15
|
+
dist/
|
|
16
|
+
build/
|
|
17
|
+
*.egg-info/
|
|
18
|
+
*.egg
|
|
19
|
+
.eggs/
|
|
20
|
+
|
|
21
|
+
# pytest
|
|
22
|
+
.pytest_cache/
|
|
23
|
+
.cache/
|
|
24
|
+
htmlcov/
|
|
25
|
+
.coverage
|
|
26
|
+
coverage.xml
|
|
27
|
+
*.cover
|
|
28
|
+
|
|
29
|
+
# mypy
|
|
30
|
+
.mypy_cache/
|
|
31
|
+
.dmypy.json
|
|
32
|
+
|
|
33
|
+
# ruff
|
|
34
|
+
.ruff_cache/
|
|
35
|
+
|
|
36
|
+
# IDEs
|
|
37
|
+
.idea/
|
|
38
|
+
.vscode/
|
|
39
|
+
*.swp
|
|
40
|
+
*.swo
|
|
41
|
+
|
|
42
|
+
# macOS
|
|
43
|
+
.DS_Store
|
|
44
|
+
|
|
45
|
+
# Secrets / environment
|
|
46
|
+
.env
|
|
47
|
+
.env.*
|
|
48
|
+
!.env.example
|
|
49
|
+
|
|
50
|
+
# Logs
|
|
51
|
+
*.log
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
### Planned
|
|
13
|
+
- Phase 2: semantic cache (embedding-based similarity matching).
|
|
14
|
+
- Phase 3: graph-level and node-level execution reuse.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## [0.1.0] — 2026-08-22
|
|
19
|
+
|
|
20
|
+
First public release. Phase 1 — exact caching.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- `MemoryCache` — high-level client wiring together backends, caches, and stats.
|
|
24
|
+
- `ExactCache` — SHA-256 hash-keyed cache for LLM responses with gzip compression.
|
|
25
|
+
- `ToolCache` — TTL-enforced cache for tool/function call results.
|
|
26
|
+
- `InMemoryBackend` — zero-dependency in-process backend with LRU eviction and TTL support.
|
|
27
|
+
- `RedisBackend` — async Redis backend with connection pooling (optional `[redis]` extra).
|
|
28
|
+
- `CacheConfig` — dataclass-based configuration with `from_env()` factory reading
|
|
29
|
+
`MEMORY_REUSE_*` environment variables.
|
|
30
|
+
- Multi-scope support: `global`, `user`, `session`.
|
|
31
|
+
- `ScopeViolationError` — raised when user-scoped data would be cached without a `user_id`.
|
|
32
|
+
- `CacheStats` / `StatsTracker` — hit/miss/error counters with a `hit_rate` property.
|
|
33
|
+
- `cached_node` decorator — LangGraph node output caching.
|
|
34
|
+
- `cached_tool` decorator — function/tool return-value caching (any framework).
|
|
35
|
+
- `cached_litellm_completion` / `cached_litellm_embedding` — cached wrappers for LiteLLM,
|
|
36
|
+
supporting OpenAI, Anthropic, AWS Bedrock, Groq, Ollama, and 100+ providers
|
|
37
|
+
(optional `[litellm]` extra).
|
|
38
|
+
- All decorators and wrappers support both sync and async callables.
|
|
39
|
+
- Shipped type hints (`py.typed`, PEP 561).
|
|
40
|
+
- Examples: basic exact cache, LangGraph agent, and a real LangGraph agent with a
|
|
41
|
+
calculator and web-search tool.
|
|
42
|
+
|
|
43
|
+
[Unreleased]: https://github.com/pranit-p/memory-reuse/compare/v0.1.0...HEAD
|
|
44
|
+
[0.1.0]: https://github.com/pranit-p/memory-reuse/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pranit Pawar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: memory-reuse
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Execution cache layer for AI agents — cut LLM and tool call costs by reusing previous results.
|
|
5
|
+
Project-URL: Homepage, https://github.com/pranit-p/memory-reuse
|
|
6
|
+
Project-URL: Documentation, https://github.com/pranit-p/memory-reuse#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/pranit-p/memory-reuse
|
|
8
|
+
Project-URL: Issues, https://github.com/pranit-p/memory-reuse/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/pranit-p/memory-reuse/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Pranit <your-email@example.com>
|
|
11
|
+
Maintainer-email: Pranit <your-email@example.com>
|
|
12
|
+
License-Expression: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: agents,ai,cache,caching,cost-reduction,langgraph,litellm,llm,redis,semantic-cache
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Provides-Extra: all
|
|
28
|
+
Requires-Dist: litellm>=1.40.0; extra == 'all'
|
|
29
|
+
Requires-Dist: redis>=5.0.0; extra == 'all'
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: black>=24.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
37
|
+
Provides-Extra: litellm
|
|
38
|
+
Requires-Dist: litellm>=1.40.0; extra == 'litellm'
|
|
39
|
+
Provides-Extra: redis
|
|
40
|
+
Requires-Dist: redis>=5.0.0; extra == 'redis'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# memory-reuse
|
|
44
|
+
|
|
45
|
+
[](https://github.com/pranit-p/memory-reuse/actions/workflows/test.yml)
|
|
46
|
+
[](https://pypi.org/project/memory-reuse/)
|
|
47
|
+
[](https://pypi.org/project/memory-reuse/)
|
|
48
|
+
[](LICENSE)
|
|
49
|
+
[](https://github.com/psf/black)
|
|
50
|
+
|
|
51
|
+
An execution cache layer for AI agents that cuts LLM and tool call costs by avoiding redundant computation. Drop it into any Python agent or LangGraph workflow with a single decorator.
|
|
52
|
+
|
|
53
|
+
- **Framework-agnostic** — LangGraph, LiteLLM, or any plain Python function.
|
|
54
|
+
- **Zero required dependencies** — the core runs on the standard library alone.
|
|
55
|
+
- **Safe by default** — per-user / per-session scoping prevents cross-user cache leaks.
|
|
56
|
+
- **Typed** — ships with `py.typed`, fully type-hinted.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## How it works
|
|
61
|
+
|
|
62
|
+
When your agent calls an LLM or a tool, `memory-reuse` hashes the inputs and
|
|
63
|
+
checks the cache first. On a hit it returns the stored result instantly — no
|
|
64
|
+
tokens spent, no API call made. On a miss it runs the real call and stores the
|
|
65
|
+
result for next time.
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
request ──► hash inputs ──► cache lookup
|
|
69
|
+
├── HIT ──► return cached result (0 cost)
|
|
70
|
+
└── MISS ──► run LLM/tool ──► store ──► return
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> **Current scope (v0.1):** exact-match caching — identical inputs hit the
|
|
74
|
+
> cache. Semantic caching (similar-but-not-identical inputs) is on the
|
|
75
|
+
> [roadmap](#roadmap).
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Install
|
|
80
|
+
|
|
81
|
+
Works with both **pip** and **uv** — pick whichever you use.
|
|
82
|
+
|
|
83
|
+
**pip**
|
|
84
|
+
```bash
|
|
85
|
+
pip install memory-reuse
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**uv**
|
|
89
|
+
```bash
|
|
90
|
+
uv add memory-reuse
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Optional extras
|
|
94
|
+
|
|
95
|
+
| Extra | What it adds | pip | uv |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| `redis` | Redis backend support | `pip install memory-reuse[redis]` | `uv add memory-reuse[redis]` |
|
|
98
|
+
| `litellm` | LiteLLM cached wrappers | `pip install memory-reuse[litellm]` | `uv add memory-reuse[litellm]` |
|
|
99
|
+
| `all` | Everything above | `pip install memory-reuse[all]` | `uv add memory-reuse[all]` |
|
|
100
|
+
|
|
101
|
+
> **Note:** `uv` is a fast Python package manager. If you don't have it yet:
|
|
102
|
+
> `pip install uv` or see [docs.astral.sh/uv](https://docs.astral.sh/uv/getting-started/installation/)
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Quick start
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from memory_reuse import MemoryCache, CacheConfig
|
|
110
|
+
from memory_reuse.integrations import cached_tool
|
|
111
|
+
|
|
112
|
+
cache = MemoryCache() # in-memory backend, 1-hour TTL
|
|
113
|
+
|
|
114
|
+
@cached_tool(cache, scope="global", ttl=300) # cache for 5 minutes
|
|
115
|
+
async def search_web(query: str) -> list[str]:
|
|
116
|
+
return await my_search_api(query) # only called on cache miss
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Usage patterns
|
|
122
|
+
|
|
123
|
+
### 1 — Basic exact cache (LLM responses)
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from memory_reuse import MemoryCache
|
|
127
|
+
|
|
128
|
+
cache = MemoryCache()
|
|
129
|
+
|
|
130
|
+
# Manual get/set
|
|
131
|
+
result = await cache.exact.get(["gpt-4", prompt], scope="global", scope_id=None)
|
|
132
|
+
if result is None:
|
|
133
|
+
result = await llm.ainvoke(prompt)
|
|
134
|
+
await cache.exact.set(["gpt-4", prompt], result, scope="global",
|
|
135
|
+
scope_id=None, ttl=3600)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 2 — LangGraph node caching
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from memory_reuse.integrations import cached_node
|
|
142
|
+
|
|
143
|
+
@cached_node(cache, scope="user", key_fields=["messages"])
|
|
144
|
+
async def summarise(state: dict) -> dict:
|
|
145
|
+
summary = await llm.ainvoke(state["messages"])
|
|
146
|
+
return {"summary": summary}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The decorator reads `user_id` from the state dict automatically, or from
|
|
150
|
+
`cache.set_context(user_id=...)`.
|
|
151
|
+
|
|
152
|
+
### 3 — LangGraph tool caching
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from memory_reuse.integrations import cached_tool
|
|
156
|
+
|
|
157
|
+
@cached_tool(cache, scope="session", ttl=120)
|
|
158
|
+
async def fetch_user_profile(user_id: str) -> dict:
|
|
159
|
+
return await db.get_user(user_id)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### 4 — LiteLLM (works with OpenAI, Claude, Bedrock, Groq, Ollama, and 100+ more)
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from memory_reuse.integrations import cached_litellm_completion, cached_litellm_embedding
|
|
166
|
+
|
|
167
|
+
# Completion — same prompt + model = cache hit, 0 tokens used
|
|
168
|
+
response = await cached_litellm_completion(
|
|
169
|
+
cache,
|
|
170
|
+
model="gpt-4o-mini", # swap for any LiteLLM model string
|
|
171
|
+
messages=[{"role": "user", "content": "What is the capital of France?"}],
|
|
172
|
+
ttl=3600,
|
|
173
|
+
scope="global",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Embeddings — deterministic, safe to cache for 24 hours
|
|
177
|
+
embeddings = await cached_litellm_embedding(
|
|
178
|
+
cache,
|
|
179
|
+
model="text-embedding-3-small",
|
|
180
|
+
input=["What is machine learning?"],
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Backend options
|
|
187
|
+
|
|
188
|
+
| Backend | Extra required | Persistence | Notes |
|
|
189
|
+
|---------|---------------|-------------|-------|
|
|
190
|
+
| `memory` | none | in-process only | LRU eviction, TTL support |
|
|
191
|
+
| `redis` | `[redis]` | yes | connection pool, lazy connect |
|
|
192
|
+
|
|
193
|
+
Configure via code or environment variables:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
export MEMORY_REUSE_BACKEND=redis
|
|
197
|
+
export MEMORY_REUSE_REDIS_URL=redis://localhost:6379/0
|
|
198
|
+
export MEMORY_REUSE_DEFAULT_TTL=600
|
|
199
|
+
export MEMORY_REUSE_DEFAULT_SCOPE=user
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
cache = MemoryCache.from_env()
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Multi-scope support
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
cache.set_context(user_id="alice", session_id="sess-001")
|
|
212
|
+
|
|
213
|
+
# User-scoped: alice cannot see bob's cache
|
|
214
|
+
await cache.exact.get(["key"], scope="user", scope_id="alice")
|
|
215
|
+
|
|
216
|
+
# Session-scoped: isolated per conversation
|
|
217
|
+
await cache.tool.get("search", args, scope="session", scope_id="sess-001")
|
|
218
|
+
|
|
219
|
+
# Global: shared across all users — safe for public, stateless data
|
|
220
|
+
await cache.exact.get(["key"], scope="global", scope_id=None)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Using `scope="user"` without a `user_id` raises `ScopeViolationError` to
|
|
224
|
+
prevent accidental cross-user data leaks.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Cache statistics
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
stats = cache.stats
|
|
232
|
+
print(f"Hit rate: {stats.hit_rate:.1%}")
|
|
233
|
+
print(f"Hits: {stats.hits} Misses: {stats.misses}")
|
|
234
|
+
print(stats.to_dict())
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Examples
|
|
240
|
+
|
|
241
|
+
Runnable examples live in [`examples/`](examples/):
|
|
242
|
+
|
|
243
|
+
- `basic_exact_cache.py` — the cache primitives with no framework.
|
|
244
|
+
- `langgraph_agent_example.py` — cached nodes and tools in a LangGraph-style flow.
|
|
245
|
+
- `langgraph_math_agent.py` — a real ReAct agent with a **calculator** and a
|
|
246
|
+
**web-search** tool, calling an LLM via LiteLLM.
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
export API_KEY="your-groq-key" # example uses Groq via LiteLLM
|
|
250
|
+
python examples/langgraph_math_agent.py
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Roadmap
|
|
256
|
+
|
|
257
|
+
| Phase | Feature | Status |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| 1 | Exact cache (LLM + tool), Redis backend, LangGraph + LiteLLM | ✅ Shipped in v0.1 |
|
|
260
|
+
| 2 | Semantic cache (embedding similarity, configurable threshold) | Planned |
|
|
261
|
+
| 3 | Graph-level and node-level execution reuse | Planned |
|
|
262
|
+
| 4 | Analytics dashboard, more framework integrations | Planned |
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Contributing
|
|
267
|
+
|
|
268
|
+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup,
|
|
269
|
+
tests, and code-style guidelines, and [CONTRIBUTORS.md](CONTRIBUTORS.md) for the
|
|
270
|
+
list of people who have helped build this project.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
[MIT](LICENSE) © Pranit Pawar
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# memory-reuse
|
|
2
|
+
|
|
3
|
+
[](https://github.com/pranit-p/memory-reuse/actions/workflows/test.yml)
|
|
4
|
+
[](https://pypi.org/project/memory-reuse/)
|
|
5
|
+
[](https://pypi.org/project/memory-reuse/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://github.com/psf/black)
|
|
8
|
+
|
|
9
|
+
An execution cache layer for AI agents that cuts LLM and tool call costs by avoiding redundant computation. Drop it into any Python agent or LangGraph workflow with a single decorator.
|
|
10
|
+
|
|
11
|
+
- **Framework-agnostic** — LangGraph, LiteLLM, or any plain Python function.
|
|
12
|
+
- **Zero required dependencies** — the core runs on the standard library alone.
|
|
13
|
+
- **Safe by default** — per-user / per-session scoping prevents cross-user cache leaks.
|
|
14
|
+
- **Typed** — ships with `py.typed`, fully type-hinted.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
|
|
20
|
+
When your agent calls an LLM or a tool, `memory-reuse` hashes the inputs and
|
|
21
|
+
checks the cache first. On a hit it returns the stored result instantly — no
|
|
22
|
+
tokens spent, no API call made. On a miss it runs the real call and stores the
|
|
23
|
+
result for next time.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
request ──► hash inputs ──► cache lookup
|
|
27
|
+
├── HIT ──► return cached result (0 cost)
|
|
28
|
+
└── MISS ──► run LLM/tool ──► store ──► return
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
> **Current scope (v0.1):** exact-match caching — identical inputs hit the
|
|
32
|
+
> cache. Semantic caching (similar-but-not-identical inputs) is on the
|
|
33
|
+
> [roadmap](#roadmap).
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
Works with both **pip** and **uv** — pick whichever you use.
|
|
40
|
+
|
|
41
|
+
**pip**
|
|
42
|
+
```bash
|
|
43
|
+
pip install memory-reuse
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**uv**
|
|
47
|
+
```bash
|
|
48
|
+
uv add memory-reuse
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Optional extras
|
|
52
|
+
|
|
53
|
+
| Extra | What it adds | pip | uv |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| `redis` | Redis backend support | `pip install memory-reuse[redis]` | `uv add memory-reuse[redis]` |
|
|
56
|
+
| `litellm` | LiteLLM cached wrappers | `pip install memory-reuse[litellm]` | `uv add memory-reuse[litellm]` |
|
|
57
|
+
| `all` | Everything above | `pip install memory-reuse[all]` | `uv add memory-reuse[all]` |
|
|
58
|
+
|
|
59
|
+
> **Note:** `uv` is a fast Python package manager. If you don't have it yet:
|
|
60
|
+
> `pip install uv` or see [docs.astral.sh/uv](https://docs.astral.sh/uv/getting-started/installation/)
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Quick start
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from memory_reuse import MemoryCache, CacheConfig
|
|
68
|
+
from memory_reuse.integrations import cached_tool
|
|
69
|
+
|
|
70
|
+
cache = MemoryCache() # in-memory backend, 1-hour TTL
|
|
71
|
+
|
|
72
|
+
@cached_tool(cache, scope="global", ttl=300) # cache for 5 minutes
|
|
73
|
+
async def search_web(query: str) -> list[str]:
|
|
74
|
+
return await my_search_api(query) # only called on cache miss
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Usage patterns
|
|
80
|
+
|
|
81
|
+
### 1 — Basic exact cache (LLM responses)
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from memory_reuse import MemoryCache
|
|
85
|
+
|
|
86
|
+
cache = MemoryCache()
|
|
87
|
+
|
|
88
|
+
# Manual get/set
|
|
89
|
+
result = await cache.exact.get(["gpt-4", prompt], scope="global", scope_id=None)
|
|
90
|
+
if result is None:
|
|
91
|
+
result = await llm.ainvoke(prompt)
|
|
92
|
+
await cache.exact.set(["gpt-4", prompt], result, scope="global",
|
|
93
|
+
scope_id=None, ttl=3600)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 2 — LangGraph node caching
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from memory_reuse.integrations import cached_node
|
|
100
|
+
|
|
101
|
+
@cached_node(cache, scope="user", key_fields=["messages"])
|
|
102
|
+
async def summarise(state: dict) -> dict:
|
|
103
|
+
summary = await llm.ainvoke(state["messages"])
|
|
104
|
+
return {"summary": summary}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The decorator reads `user_id` from the state dict automatically, or from
|
|
108
|
+
`cache.set_context(user_id=...)`.
|
|
109
|
+
|
|
110
|
+
### 3 — LangGraph tool caching
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from memory_reuse.integrations import cached_tool
|
|
114
|
+
|
|
115
|
+
@cached_tool(cache, scope="session", ttl=120)
|
|
116
|
+
async def fetch_user_profile(user_id: str) -> dict:
|
|
117
|
+
return await db.get_user(user_id)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 4 — LiteLLM (works with OpenAI, Claude, Bedrock, Groq, Ollama, and 100+ more)
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from memory_reuse.integrations import cached_litellm_completion, cached_litellm_embedding
|
|
124
|
+
|
|
125
|
+
# Completion — same prompt + model = cache hit, 0 tokens used
|
|
126
|
+
response = await cached_litellm_completion(
|
|
127
|
+
cache,
|
|
128
|
+
model="gpt-4o-mini", # swap for any LiteLLM model string
|
|
129
|
+
messages=[{"role": "user", "content": "What is the capital of France?"}],
|
|
130
|
+
ttl=3600,
|
|
131
|
+
scope="global",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Embeddings — deterministic, safe to cache for 24 hours
|
|
135
|
+
embeddings = await cached_litellm_embedding(
|
|
136
|
+
cache,
|
|
137
|
+
model="text-embedding-3-small",
|
|
138
|
+
input=["What is machine learning?"],
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Backend options
|
|
145
|
+
|
|
146
|
+
| Backend | Extra required | Persistence | Notes |
|
|
147
|
+
|---------|---------------|-------------|-------|
|
|
148
|
+
| `memory` | none | in-process only | LRU eviction, TTL support |
|
|
149
|
+
| `redis` | `[redis]` | yes | connection pool, lazy connect |
|
|
150
|
+
|
|
151
|
+
Configure via code or environment variables:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
export MEMORY_REUSE_BACKEND=redis
|
|
155
|
+
export MEMORY_REUSE_REDIS_URL=redis://localhost:6379/0
|
|
156
|
+
export MEMORY_REUSE_DEFAULT_TTL=600
|
|
157
|
+
export MEMORY_REUSE_DEFAULT_SCOPE=user
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
cache = MemoryCache.from_env()
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Multi-scope support
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
cache.set_context(user_id="alice", session_id="sess-001")
|
|
170
|
+
|
|
171
|
+
# User-scoped: alice cannot see bob's cache
|
|
172
|
+
await cache.exact.get(["key"], scope="user", scope_id="alice")
|
|
173
|
+
|
|
174
|
+
# Session-scoped: isolated per conversation
|
|
175
|
+
await cache.tool.get("search", args, scope="session", scope_id="sess-001")
|
|
176
|
+
|
|
177
|
+
# Global: shared across all users — safe for public, stateless data
|
|
178
|
+
await cache.exact.get(["key"], scope="global", scope_id=None)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Using `scope="user"` without a `user_id` raises `ScopeViolationError` to
|
|
182
|
+
prevent accidental cross-user data leaks.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Cache statistics
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
stats = cache.stats
|
|
190
|
+
print(f"Hit rate: {stats.hit_rate:.1%}")
|
|
191
|
+
print(f"Hits: {stats.hits} Misses: {stats.misses}")
|
|
192
|
+
print(stats.to_dict())
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Examples
|
|
198
|
+
|
|
199
|
+
Runnable examples live in [`examples/`](examples/):
|
|
200
|
+
|
|
201
|
+
- `basic_exact_cache.py` — the cache primitives with no framework.
|
|
202
|
+
- `langgraph_agent_example.py` — cached nodes and tools in a LangGraph-style flow.
|
|
203
|
+
- `langgraph_math_agent.py` — a real ReAct agent with a **calculator** and a
|
|
204
|
+
**web-search** tool, calling an LLM via LiteLLM.
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
export API_KEY="your-groq-key" # example uses Groq via LiteLLM
|
|
208
|
+
python examples/langgraph_math_agent.py
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Roadmap
|
|
214
|
+
|
|
215
|
+
| Phase | Feature | Status |
|
|
216
|
+
|---|---|---|
|
|
217
|
+
| 1 | Exact cache (LLM + tool), Redis backend, LangGraph + LiteLLM | ✅ Shipped in v0.1 |
|
|
218
|
+
| 2 | Semantic cache (embedding similarity, configurable threshold) | Planned |
|
|
219
|
+
| 3 | Graph-level and node-level execution reuse | Planned |
|
|
220
|
+
| 4 | Analytics dashboard, more framework integrations | Planned |
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Contributing
|
|
225
|
+
|
|
226
|
+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup,
|
|
227
|
+
tests, and code-style guidelines, and [CONTRIBUTORS.md](CONTRIBUTORS.md) for the
|
|
228
|
+
list of people who have helped build this project.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
[MIT](LICENSE) © Pranit Pawar
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Example: Using ExactCache to avoid redundant LLM calls.
|
|
2
|
+
|
|
3
|
+
Run this example:
|
|
4
|
+
|
|
5
|
+
python examples/basic_exact_cache.py
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
from memory_reuse import CacheConfig, MemoryCache
|
|
14
|
+
|
|
15
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def fake_llm_call(prompt: str) -> str:
|
|
19
|
+
"""Simulate an expensive LLM API call."""
|
|
20
|
+
print(f" [LLM] Calling API for: {prompt!r}")
|
|
21
|
+
await asyncio.sleep(0.1) # Simulate network latency
|
|
22
|
+
return f"LLM response for: {prompt}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def main() -> None:
|
|
26
|
+
# Create a cache with the in-memory backend and a 1-hour TTL
|
|
27
|
+
config = CacheConfig(backend="memory", default_ttl=3600)
|
|
28
|
+
cache = MemoryCache(config)
|
|
29
|
+
|
|
30
|
+
prompt = "Summarise the benefits of caching in AI agents"
|
|
31
|
+
key_parts = ["gpt-4", prompt]
|
|
32
|
+
|
|
33
|
+
print("\n=== First call (cache miss) ===")
|
|
34
|
+
result = await cache.exact.get(key_parts, scope="global", scope_id=None)
|
|
35
|
+
if result is None:
|
|
36
|
+
result = await fake_llm_call(prompt)
|
|
37
|
+
await cache.exact.set(key_parts, result, scope="global", scope_id=None, ttl=None)
|
|
38
|
+
print(f" Result: {result!r}")
|
|
39
|
+
|
|
40
|
+
print("\n=== Second call (cache hit — LLM not called) ===")
|
|
41
|
+
result = await cache.exact.get(key_parts, scope="global", scope_id=None)
|
|
42
|
+
if result is None:
|
|
43
|
+
result = await fake_llm_call(prompt)
|
|
44
|
+
await cache.exact.set(key_parts, result, scope="global", scope_id=None, ttl=None)
|
|
45
|
+
print(f" Result: {result!r}")
|
|
46
|
+
|
|
47
|
+
# Per-user scoped cache example
|
|
48
|
+
print("\n=== User-scoped cache ===")
|
|
49
|
+
cache.set_context(user_id="alice")
|
|
50
|
+
user_key = ["user-preference", "theme"]
|
|
51
|
+
await cache.exact.set(user_key, "dark", scope="user", scope_id="alice", ttl=None)
|
|
52
|
+
alice_pref = await cache.exact.get(user_key, scope="user", scope_id="alice")
|
|
53
|
+
bob_pref = await cache.exact.get(user_key, scope="user", scope_id="bob")
|
|
54
|
+
print(f" Alice's preference: {alice_pref}") # "dark"
|
|
55
|
+
print(f" Bob's preference: {bob_pref}") # None — isolated
|
|
56
|
+
|
|
57
|
+
print("\n=== Stats ===")
|
|
58
|
+
stats = cache.stats
|
|
59
|
+
print(f" Hits: {stats.hits}")
|
|
60
|
+
print(f" Misses: {stats.misses}")
|
|
61
|
+
print(f" Hit rate: {stats.hit_rate:.1%}")
|
|
62
|
+
|
|
63
|
+
await cache.close()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
asyncio.run(main())
|