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.
Files changed (37) hide show
  1. memory_reuse-0.1.0/.gitignore +51 -0
  2. memory_reuse-0.1.0/CHANGELOG.md +44 -0
  3. memory_reuse-0.1.0/LICENSE +21 -0
  4. memory_reuse-0.1.0/PKG-INFO +276 -0
  5. memory_reuse-0.1.0/README.md +234 -0
  6. memory_reuse-0.1.0/examples/basic_exact_cache.py +67 -0
  7. memory_reuse-0.1.0/examples/langgraph_agent_example.py +107 -0
  8. memory_reuse-0.1.0/examples/langgraph_math_agent.py +433 -0
  9. memory_reuse-0.1.0/memory_reuse/__init__.py +44 -0
  10. memory_reuse-0.1.0/memory_reuse/_utils.py +125 -0
  11. memory_reuse-0.1.0/memory_reuse/backends/__init__.py +21 -0
  12. memory_reuse-0.1.0/memory_reuse/backends/base.py +76 -0
  13. memory_reuse-0.1.0/memory_reuse/backends/memory.py +170 -0
  14. memory_reuse-0.1.0/memory_reuse/backends/redis.py +217 -0
  15. memory_reuse-0.1.0/memory_reuse/cache/__init__.py +12 -0
  16. memory_reuse-0.1.0/memory_reuse/cache/exact.py +194 -0
  17. memory_reuse-0.1.0/memory_reuse/cache/tool.py +170 -0
  18. memory_reuse-0.1.0/memory_reuse/config.py +99 -0
  19. memory_reuse-0.1.0/memory_reuse/core.py +224 -0
  20. memory_reuse-0.1.0/memory_reuse/exceptions.py +42 -0
  21. memory_reuse-0.1.0/memory_reuse/integrations/__init__.py +26 -0
  22. memory_reuse-0.1.0/memory_reuse/integrations/langgraph.py +235 -0
  23. memory_reuse-0.1.0/memory_reuse/integrations/litellm.py +277 -0
  24. memory_reuse-0.1.0/memory_reuse/py.typed +0 -0
  25. memory_reuse-0.1.0/memory_reuse/stats.py +112 -0
  26. memory_reuse-0.1.0/pyproject.toml +109 -0
  27. memory_reuse-0.1.0/tests/__init__.py +0 -0
  28. memory_reuse-0.1.0/tests/conftest.py +48 -0
  29. memory_reuse-0.1.0/tests/integration/__init__.py +0 -0
  30. memory_reuse-0.1.0/tests/integration/test_langgraph_integration.py +220 -0
  31. memory_reuse-0.1.0/tests/unit/__init__.py +0 -0
  32. memory_reuse-0.1.0/tests/unit/test_backends.py +123 -0
  33. memory_reuse-0.1.0/tests/unit/test_exact_cache.py +108 -0
  34. memory_reuse-0.1.0/tests/unit/test_litellm_integration.py +338 -0
  35. memory_reuse-0.1.0/tests/unit/test_stats.py +100 -0
  36. memory_reuse-0.1.0/tests/unit/test_tool_cache.py +103 -0
  37. 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
+ [![CI](https://github.com/pranit-p/memory-reuse/actions/workflows/test.yml/badge.svg)](https://github.com/pranit-p/memory-reuse/actions/workflows/test.yml)
46
+ [![PyPI](https://img.shields.io/pypi/v/memory-reuse.svg)](https://pypi.org/project/memory-reuse/)
47
+ [![Python](https://img.shields.io/pypi/pyversions/memory-reuse.svg)](https://pypi.org/project/memory-reuse/)
48
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
49
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](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
+ [![CI](https://github.com/pranit-p/memory-reuse/actions/workflows/test.yml/badge.svg)](https://github.com/pranit-p/memory-reuse/actions/workflows/test.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/memory-reuse.svg)](https://pypi.org/project/memory-reuse/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/memory-reuse.svg)](https://pypi.org/project/memory-reuse/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](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())