longcache 0.1.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.
- longcache/__init__.py +11 -0
- longcache/backends/__init__.py +21 -0
- longcache/backends/async_redis.py +274 -0
- longcache/backends/base.py +167 -0
- longcache/backends/memory.py +243 -0
- longcache/backends/redis.py +374 -0
- longcache/backends/sqlite.py +521 -0
- longcache/cli.py +216 -0
- longcache/config.py +127 -0
- longcache/core/__init__.py +8 -0
- longcache/core/base_embedder.py +43 -0
- longcache/core/cache.py +872 -0
- longcache/core/embedder.py +155 -0
- longcache/core/entry.py +122 -0
- longcache/core/index.py +272 -0
- longcache/integrations/__init__.py +32 -0
- longcache/integrations/langchain.py +316 -0
- longcache/integrations/langgraph.py +150 -0
- longcache/integrations/llamaindex.py +103 -0
- longcache/integrations/openai.py +146 -0
- longcache/middleware.py +366 -0
- longcache-0.1.0.dist-info/METADATA +942 -0
- longcache-0.1.0.dist-info/RECORD +26 -0
- longcache-0.1.0.dist-info/WHEEL +4 -0
- longcache-0.1.0.dist-info/entry_points.txt +2 -0
- longcache-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: longcache
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Semantic similarity cache for LLM and RAG pipelines โ cut repeat query costs by 40%+
|
|
5
|
+
Project-URL: Repository, https://github.com/ENDEVSOLS/LongCache
|
|
6
|
+
Project-URL: Documentation, https://endevsols.github.io/LongCache
|
|
7
|
+
Project-URL: Changelog, https://github.com/ENDEVSOLS/LongCache/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/ENDEVSOLS/LongCache/issues
|
|
9
|
+
Author-email: ENDEVSOLS <technology@endevsols.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: cache,cost-reduction,embeddings,faiss,langchain,langgraph,latency-optimization,llamaindex,llm,openai,rag,semantic-cache,sentence-transformers,vector-search
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: click>=8.1.0
|
|
25
|
+
Requires-Dist: faiss-cpu>=1.7.4
|
|
26
|
+
Requires-Dist: numpy<2.0.0,>=1.24.0
|
|
27
|
+
Requires-Dist: sentence-transformers>=2.3.0
|
|
28
|
+
Requires-Dist: torch
|
|
29
|
+
Provides-Extra: all
|
|
30
|
+
Requires-Dist: fastapi>=0.109.0; extra == 'all'
|
|
31
|
+
Requires-Dist: langchain-core>=0.3.0; extra == 'all'
|
|
32
|
+
Requires-Dist: langgraph>=0.2.0; extra == 'all'
|
|
33
|
+
Requires-Dist: llama-index-core>=0.10.0; extra == 'all'
|
|
34
|
+
Requires-Dist: openai>=1.0.0; extra == 'all'
|
|
35
|
+
Requires-Dist: redis<6.0.0,>=5.0.0; extra == 'all'
|
|
36
|
+
Requires-Dist: starlette>=0.36.0; extra == 'all'
|
|
37
|
+
Requires-Dist: uvicorn>=0.27.0; extra == 'all'
|
|
38
|
+
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: build>=1.1.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: httpx>=0.27.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: mypy>=1.9.0; extra == 'dev'
|
|
42
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
43
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
|
|
44
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
45
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
46
|
+
Requires-Dist: twine>=5.0.0; extra == 'dev'
|
|
47
|
+
Provides-Extra: docs
|
|
48
|
+
Requires-Dist: mkdocs-material>=9.5.0; extra == 'docs'
|
|
49
|
+
Requires-Dist: mkdocstrings[python]>=0.24.0; extra == 'docs'
|
|
50
|
+
Provides-Extra: langchain
|
|
51
|
+
Requires-Dist: langchain-core>=0.3.0; extra == 'langchain'
|
|
52
|
+
Requires-Dist: langgraph>=0.2.0; extra == 'langchain'
|
|
53
|
+
Provides-Extra: llamaindex
|
|
54
|
+
Requires-Dist: llama-index-core>=0.10.0; extra == 'llamaindex'
|
|
55
|
+
Provides-Extra: openai
|
|
56
|
+
Requires-Dist: openai>=1.0.0; extra == 'openai'
|
|
57
|
+
Provides-Extra: redis
|
|
58
|
+
Requires-Dist: redis<6.0.0,>=5.0.0; extra == 'redis'
|
|
59
|
+
Provides-Extra: server
|
|
60
|
+
Requires-Dist: fastapi>=0.109.0; extra == 'server'
|
|
61
|
+
Requires-Dist: starlette>=0.36.0; extra == 'server'
|
|
62
|
+
Requires-Dist: uvicorn>=0.27.0; extra == 'server'
|
|
63
|
+
Description-Content-Type: text/markdown
|
|
64
|
+
|
|
65
|
+
<p align="center">
|
|
66
|
+
<img src="https://raw.githubusercontent.com/ENDEVSOLS/LongCache/main/assets/logo.png" alt="LongCache Logo" width="320"/>
|
|
67
|
+
</p>
|
|
68
|
+
|
|
69
|
+
<p align="center"><strong>Production-grade semantic similarity cache for LLM and RAG pipelines โ cut repeat query costs by 40%+.</strong></p>
|
|
70
|
+
|
|
71
|
+
<p align="center">
|
|
72
|
+
<a href="https://pypi.org/project/longcache/"><img src="https://img.shields.io/pypi/v/longcache" alt="PyPI Version"></a>
|
|
73
|
+
<a href="https://pepy.tech/project/longcache"><img src="https://static.pepy.tech/badge/longcache" alt="Total Downloads"></a>
|
|
74
|
+
<a href="https://pepy.tech/project/longcache"><img src="https://static.pepy.tech/badge/longcache/month" alt="Monthly Downloads"></a>
|
|
75
|
+
<a href="https://github.com/ENDEVSOLS/LongCache/stargazers"><img src="https://img.shields.io/github/stars/ENDEVSOLS/LongCache?style=flat" alt="GitHub Stars"></a>
|
|
76
|
+
<a href="https://github.com/ENDEVSOLS/LongCache/actions/workflows/ci.yml"><img src="https://github.com/ENDEVSOLS/LongCache/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
77
|
+
<img src="https://img.shields.io/pypi/pyversions/longcache" alt="Python Versions">
|
|
78
|
+
<a href="https://github.com/ENDEVSOLS/LongCache/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ENDEVSOLS/LongCache" alt="License"></a>
|
|
79
|
+
</p>
|
|
80
|
+
|
|
81
|
+
<p align="center">
|
|
82
|
+
<a href="https://endevsols.github.io/LongCache/"><strong>๐ Documentation</strong></a> ยท
|
|
83
|
+
<a href="https://endevsols.github.io/LongCache/getting-started/quickstart/"><strong>Quick Start</strong></a> ยท
|
|
84
|
+
<a href="https://endevsols.github.io/LongCache/api-reference/"><strong>API Reference</strong></a> ยท
|
|
85
|
+
<a href="https://github.com/ENDEVSOLS/LongCache/blob/main/CHANGELOG.md"><strong>Changelog</strong></a>
|
|
86
|
+
</p>
|
|
87
|
+
|
|
88
|
+
> *"What is your refund policy?" costs $0.003 the first time.*
|
|
89
|
+
> *It should cost $0.0000 every time after that.*
|
|
90
|
+
|
|
91
|
+
**LongCache** is a production-grade semantic similarity cache for LLM and RAG pipelines. Unlike string-match caches, it understands that *"How do I return a product?"* and *"What's your return process?"* deserve the same cached answer โ and serves it in milliseconds.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Why LongCache?
|
|
96
|
+
|
|
97
|
+
| Problem | Solution |
|
|
98
|
+
|---------|----------|
|
|
99
|
+
| Duplicate queries burn 30โ60% of your LLM budget | Semantic cache intercepts similar queries before they reach the model |
|
|
100
|
+
| GPTCache is unmaintained with 50+ dependencies | LongCache has 4 core deps and is actively maintained |
|
|
101
|
+
| String-match caches miss paraphrased queries | FAISS + BGE-small understands semantic meaning, not just exact tokens |
|
|
102
|
+
| Most caches are coupled to one LLM provider | Works with LangChain, LangGraph, LlamaIndex, OpenAI, or raw functions |
|
|
103
|
+
| Cross-model cache poisoning is a silent bug | Composite key scoping isolates entries per model, prompt, and temperature |
|
|
104
|
+
|
|
105
|
+
**Result: Cut repeat query costs by 40%+ in production RAG applications.**
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Quick Start
|
|
110
|
+
|
|
111
|
+
### Installation
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Core โ SQLite backend + FAISS + BGE-small (no external services needed)
|
|
115
|
+
uv pip install longcache
|
|
116
|
+
|
|
117
|
+
# With LangChain + LangGraph integration
|
|
118
|
+
uv pip install longcache[langchain]
|
|
119
|
+
|
|
120
|
+
# With Redis backend for multi-instance production
|
|
121
|
+
uv pip install longcache[redis]
|
|
122
|
+
|
|
123
|
+
# Everything
|
|
124
|
+
uv pip install longcache[all]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
> **Offline-friendly**: LongCache embeds queries locally. After the BGE-small model weights are downloaded on first run, no internet connection is required.
|
|
128
|
+
|
|
129
|
+
### 30-Second Example
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from longcache import LongCache
|
|
133
|
+
|
|
134
|
+
cache = LongCache()
|
|
135
|
+
|
|
136
|
+
@cache.cached
|
|
137
|
+
def my_rag_chain(query: str) -> str:
|
|
138
|
+
return expensive_llm_call(query) # Your retrieval + LLM pipeline
|
|
139
|
+
|
|
140
|
+
# First call: MISS โ runs full pipeline (~2 000 ms)
|
|
141
|
+
result1 = my_rag_chain("What is your refund policy?")
|
|
142
|
+
|
|
143
|
+
# Second call โ exact query: HIT โ instant (~5 ms)
|
|
144
|
+
result2 = my_rag_chain("What is your refund policy?")
|
|
145
|
+
|
|
146
|
+
# Third call โ paraphrase: HIT โ instant
|
|
147
|
+
result3 = my_rag_chain("How do I get a refund?")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Async functions are detected automatically โ no additional setup needed:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
@cache.cached(namespace="faq", ttl=7200)
|
|
154
|
+
async def my_async_chain(query: str) -> str:
|
|
155
|
+
return await async_llm_call(query)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## How It Works
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
164
|
+
โ Query Lifecycle โ
|
|
165
|
+
โ โ
|
|
166
|
+
โ User Query โ
|
|
167
|
+
โ โ โ
|
|
168
|
+
โ โผ โ
|
|
169
|
+
โ โโโโโโโโโโโโโโโโโโโโโ โ
|
|
170
|
+
โ โ LocalEmbedder โ BAAI/bge-small-en-v1.5 โ no API โ
|
|
171
|
+
โ โ (~5 ms, local) โ call, runs fully offline โ
|
|
172
|
+
โ โโโโโโโโโโฌโโโโโโโโโโโ โ
|
|
173
|
+
โ โ 384-dim vector โ
|
|
174
|
+
โ โผ โ
|
|
175
|
+
โ โโโโโโโโโโโโโโโโโโโโโ HIT (similarity โฅ threshold) โ
|
|
176
|
+
โ โ SimilarityIndex โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโถ โ
|
|
177
|
+
โ โ FAISS + Backend โ Return cached response โ
|
|
178
|
+
โ โโโโโโโโโโฌโโโโโโโโโโโ โ
|
|
179
|
+
โ โ MISS โ
|
|
180
|
+
โ โผ โ
|
|
181
|
+
โ โโโโโโโโโโโโโโโโโโโโโ โ
|
|
182
|
+
โ โ Full RAG Chain โ Retrieval + LLM (~1โ3 s) โ
|
|
183
|
+
โ โโโโโโโโโโฌโโโโโโโโโโโ โ
|
|
184
|
+
โ โ โ
|
|
185
|
+
โ โผ โ
|
|
186
|
+
โ โโโโโโโโโโโโโโโโโโโโโ โ
|
|
187
|
+
โ โ CacheWriter โ Stores vector + response + TTL โ
|
|
188
|
+
โ โ โ + namespace + tags + context โ
|
|
189
|
+
โ โโโโโโโโโโโโโโโโโโโโโ โ
|
|
190
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
1. **Intercept** โ queries are checked before they reach the LLM
|
|
194
|
+
2. **Embed** โ the query is embedded locally in ~5 ms (BGE-small, no API call)
|
|
195
|
+
3. **Search** โ FAISS searches for cosine similarity against cached vectors
|
|
196
|
+
4. **HIT** (similarity โฅ threshold): return cached response instantly
|
|
197
|
+
5. **MISS**: run the full pipeline, cache the result, and return it
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Configuration
|
|
202
|
+
|
|
203
|
+
### CacheConfig
|
|
204
|
+
|
|
205
|
+
All settings are controlled via the `CacheConfig` dataclass:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from longcache import LongCache, CacheConfig
|
|
209
|
+
|
|
210
|
+
config = CacheConfig(
|
|
211
|
+
similarity_threshold=0.92, # 0.0โ1.0; higher = stricter matching
|
|
212
|
+
ttl_seconds=3600, # Entry lifetime in seconds (0 = never expire)
|
|
213
|
+
max_entries=10_000, # Max entries per namespace (LRU eviction)
|
|
214
|
+
namespace="default", # Logical isolation group
|
|
215
|
+
embedder_model="BAAI/bge-small-en-v1.5", # Local embedding model
|
|
216
|
+
backend="sqlite", # "sqlite" | "redis" | "async_redis" | "memory"
|
|
217
|
+
db_path=".longcache/cache.db", # SQLite path (auto-creates parent dirs)
|
|
218
|
+
redis_url="redis://localhost:6379/0",# Redis connection URL
|
|
219
|
+
track_stats=True, # Track hits, misses, and cost savings
|
|
220
|
+
faiss_index_type="flat", # "flat" (exact) | "hnsw" (approximate, faster at scale)
|
|
221
|
+
hnsw_m=32, # HNSW connections per node
|
|
222
|
+
hnsw_ef_search=64, # HNSW search depth
|
|
223
|
+
# Composite key context (isolates entries per model/persona)
|
|
224
|
+
model="", # LLM model identifier (e.g. "gpt-4o")
|
|
225
|
+
system_prompt="", # System prompt string
|
|
226
|
+
temperature=-1.0, # Sampling temperature (-1.0 = unset)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
cache = LongCache(config)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### From a Dictionary
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
config = CacheConfig.from_dict({
|
|
236
|
+
"similarity_threshold": 0.90,
|
|
237
|
+
"backend": "redis",
|
|
238
|
+
"ttl_seconds": 7200,
|
|
239
|
+
})
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### From Environment Variables
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
export LONGCACHE_SIMILARITY_THRESHOLD=0.90
|
|
246
|
+
export LONGCACHE_BACKEND=redis
|
|
247
|
+
export LONGCACHE_TTL_SECONDS=7200
|
|
248
|
+
export LONGCACHE_REDIS_URL=redis://prod-redis:6379/0
|
|
249
|
+
export LONGCACHE_MODEL=gpt-4o
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
config = CacheConfig.from_env() # Reads all LONGCACHE_* env vars automatically
|
|
254
|
+
cache = LongCache(config)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Similarity Threshold Guide
|
|
258
|
+
|
|
259
|
+
| Threshold | Behavior | Recommended Use Case |
|
|
260
|
+
|-----------|----------|----------------------|
|
|
261
|
+
| 0.99 | Near-exact match only | Precise FAQ lookup |
|
|
262
|
+
| 0.95 | Very similar phrasing | Customer support |
|
|
263
|
+
| **0.92** | **Default โ balanced** | **General RAG pipelines** |
|
|
264
|
+
| 0.85 | Looser matching | Exploratory search |
|
|
265
|
+
| 0.75 | Topic-level clustering | Broad domain matching |
|
|
266
|
+
|
|
267
|
+
**Tip:** Start at 0.92 and adjust based on your observed hit rate vs. false-positive rate.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Features โจ
|
|
272
|
+
|
|
273
|
+
### Core
|
|
274
|
+
|
|
275
|
+
- โ
**Semantic Similarity Cache:** Understands paraphrased queries โ โHow do I return a product?โ hits the same cache entry as โWhatโs your return process?โ
|
|
276
|
+
- โ
**Local Embedding:** BAAI/bge-small-en-v1.5 runs entirely on-device (~5 ms per query, no API call, fully offline after first download)
|
|
277
|
+
- โ
**`@cache.cached` Decorator:** One-line caching for any sync or async function โ auto-detects coroutines
|
|
278
|
+
- โ
**`@cache.cached_stream` Decorator:** Caches streaming generator responses (sync and async); yields live on MISS, replays from cache on HIT
|
|
279
|
+
- โ
**Composite Cache Keys:** Scopes entries per `model`, `system_prompt`, and `temperature` via SHA-256 fingerprint โ prevents cross-model cache poisoning
|
|
280
|
+
- โ
**Tag-Based Invalidation:** Attach tags to entries at write time; selectively invalidate by tag without clearing entire namespaces
|
|
281
|
+
- โ
**Per-Entry TTL:** Override TTL per `set()` call or decorator โ supports never-expire (`ttl=0`) and short-lived entries
|
|
282
|
+
- โ
**LRU Eviction:** Configurable `max_entries` per namespace; oldest entries evicted automatically when the limit is reached
|
|
283
|
+
|
|
284
|
+
### Reliability
|
|
285
|
+
|
|
286
|
+
- โ
**Thread-Safe Statistics:** Hit/miss counts protected by a dedicated lock; safe under 8+ concurrent threads
|
|
287
|
+
- โ
**Persistent Stats:** Hit/miss/savings data written to SQLite and survives process restarts
|
|
288
|
+
- โ
**Rebuild-Safe FAISS Index:** Index is fully rebuilt after TTL sweeps or evictions โ prevents stale vector accumulation
|
|
289
|
+
- โ
**Background TTL Sweeper:** Daemon thread runs every 5 minutes to delete expired entries; shuts down cleanly on GC
|
|
290
|
+
- โ
**Correct Miss Counting:** `get()`, `aget()`, and all decorators record misses accurately in all code paths
|
|
291
|
+
- โ
**Config Validation:** `CacheConfig` validates all fields on construction with clear error messages
|
|
292
|
+
|
|
293
|
+
### Integrations & Backends
|
|
294
|
+
|
|
295
|
+
- โ
**SQLite Backend:** Zero external dependencies, WAL-mode thread safety, persistent stats, default for all deployments
|
|
296
|
+
- โ
**Redis Backend:** Shared cache across multiple instances/pods, atomic TTL via `EXPIRE` (sync)
|
|
297
|
+
- โ
**Async Redis Backend:** Non-blocking Redis โ safe inside `asyncio` event loops (FastAPI, etc.)
|
|
298
|
+
- โ
**In-Memory Backend:** Zero I/O, ideal for unit tests and CI/CD pipelines
|
|
299
|
+
- โ
**LangChain Integration:** `CachedLLM` and `CachedChain` โ drop-in wrappers with sync, async, and streaming support
|
|
300
|
+
- โ
**LangGraph Integration:** `CachedState` node for graph-based agents; async variants included
|
|
301
|
+
- โ
**LlamaIndex Integration:** `CachedQueryEngine` wraps any LlamaIndex query engine
|
|
302
|
+
- โ
**OpenAI Integration:** `CachedOpenAI` wraps the OpenAI client directly
|
|
303
|
+
- โ
**FastAPI Middleware:** `LongCacheMiddleware` caches entire HTTP routes; `X-LongCache: HIT` header on cached responses
|
|
304
|
+
- โ
**CLI:** `longcache stats`, `clear`, `inspect`, `export` โ manage the cache without writing code
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Backends
|
|
309
|
+
|
|
310
|
+
### SQLite (Default โ Zero External Dependencies)
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
cache = LongCache(CacheConfig(
|
|
314
|
+
backend="sqlite",
|
|
315
|
+
db_path=".longcache/cache.db", # Auto-creates parent directories
|
|
316
|
+
))
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
| | |
|
|
320
|
+
|---|---|
|
|
321
|
+
| **Pros** | Zero external deps, persistent, WAL-mode thread safety, stats survive restarts |
|
|
322
|
+
| **Cons** | Single-machine only |
|
|
323
|
+
| **Best for** | Development, single-server production, CLI tools |
|
|
324
|
+
|
|
325
|
+
### Redis (Production Multi-Instance)
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
uv pip install longcache[redis]
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
cache = LongCache(CacheConfig(
|
|
333
|
+
backend="redis",
|
|
334
|
+
redis_url="redis://localhost:6379/0",
|
|
335
|
+
max_entries=100_000,
|
|
336
|
+
))
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
| | |
|
|
340
|
+
|---|---|
|
|
341
|
+
| **Pros** | Shared across instances, atomic TTL via `EXPIRE`, highly scalable |
|
|
342
|
+
| **Cons** | Requires Redis; blocking I/O (not suited for `async` event loops) |
|
|
343
|
+
| **Best for** | Multi-instance Kubernetes deployments, microservices (sync) |
|
|
344
|
+
|
|
345
|
+
### Async Redis (Production Async Apps)
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
cache = LongCache(CacheConfig(
|
|
349
|
+
backend="async_redis",
|
|
350
|
+
redis_url="redis://localhost:6379/0",
|
|
351
|
+
))
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
| | |
|
|
355
|
+
|---|---|
|
|
356
|
+
| **Pros** | Non-blocking โ safe inside `async` event loops (FastAPI, etc.) |
|
|
357
|
+
| **Best for** | Fully async production deployments |
|
|
358
|
+
|
|
359
|
+
### In-Memory (Testing & CI)
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
cache = LongCache(CacheConfig(backend="memory"))
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
| | |
|
|
366
|
+
|---|---|
|
|
367
|
+
| **Pros** | Fastest possible, zero I/O, no files created |
|
|
368
|
+
| **Cons** | Lost on process restart, single-process only |
|
|
369
|
+
| **Best for** | Unit tests, CI/CD pipelines, benchmarks |
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Integrations
|
|
374
|
+
|
|
375
|
+
### LangChain
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
uv pip install longcache[langchain]
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
#### `CachedLLM` โ Wrap Any LLM
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
from langchain_openai import ChatOpenAI
|
|
385
|
+
from longcache.integrations.langchain import CachedLLM
|
|
386
|
+
|
|
387
|
+
llm = CachedLLM(ChatOpenAI(model="gpt-4o"), cache=LongCache())
|
|
388
|
+
|
|
389
|
+
response = llm.invoke("What is the refund policy?") # sync
|
|
390
|
+
response = await llm.ainvoke("What is the refund policy?") # async
|
|
391
|
+
|
|
392
|
+
for chunk in llm.stream("What is RAG?"): # sync streaming
|
|
393
|
+
print(chunk, end="")
|
|
394
|
+
|
|
395
|
+
async for chunk in llm.astream("What is RAG?"): # async streaming
|
|
396
|
+
print(chunk, end="")
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
#### `CachedChain` โ Wrap Any LCEL Chain
|
|
400
|
+
|
|
401
|
+
```python
|
|
402
|
+
from langchain_openai import ChatOpenAI
|
|
403
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
404
|
+
from langchain_core.output_parsers import StrOutputParser
|
|
405
|
+
from longcache.integrations.langchain import CachedChain
|
|
406
|
+
|
|
407
|
+
chain = ChatPromptTemplate.from_template("Answer: {question}") | ChatOpenAI() | StrOutputParser()
|
|
408
|
+
cached_chain = CachedChain(chain, cache=LongCache(), query_key="question")
|
|
409
|
+
|
|
410
|
+
result = cached_chain.invoke({"question": "What is RAG?"})
|
|
411
|
+
result = await cached_chain.ainvoke({"question": "What is RAG?"})
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### LangGraph
|
|
415
|
+
|
|
416
|
+
```python
|
|
417
|
+
from langgraph.graph import StateGraph
|
|
418
|
+
from longcache import LongCache
|
|
419
|
+
from longcache.integrations.langgraph import CachedState
|
|
420
|
+
|
|
421
|
+
cache = LongCache()
|
|
422
|
+
|
|
423
|
+
def cache_node(state: dict) -> dict:
|
|
424
|
+
return CachedState.check(state, cache, query_key="messages")
|
|
425
|
+
|
|
426
|
+
def llm_node(state: dict) -> dict:
|
|
427
|
+
response = my_llm_call(state["messages"])
|
|
428
|
+
CachedState.save(state, response, cache)
|
|
429
|
+
return {"response": response}
|
|
430
|
+
|
|
431
|
+
def should_use_llm(state: dict) -> str:
|
|
432
|
+
return "llm" if not state.get("__cache_hit") else "end"
|
|
433
|
+
|
|
434
|
+
graph = StateGraph(dict)
|
|
435
|
+
graph.add_node("cache", cache_node)
|
|
436
|
+
graph.add_node("llm", llm_node)
|
|
437
|
+
graph.add_conditional_edges("cache", should_use_llm)
|
|
438
|
+
graph.set_entry_point("cache")
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Async variants: `CachedState.acheck()` and `CachedState.asave()`.
|
|
442
|
+
|
|
443
|
+
### LlamaIndex
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
uv pip install longcache[llamaindex]
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
```python
|
|
450
|
+
from llama_index.core import VectorStoreIndex
|
|
451
|
+
from longcache.integrations.llamaindex import CachedQueryEngine
|
|
452
|
+
|
|
453
|
+
engine = VectorStoreIndex.from_documents(documents).as_query_engine()
|
|
454
|
+
cached_engine = CachedQueryEngine(engine, cache=LongCache())
|
|
455
|
+
response = cached_engine.query("What is the refund policy?")
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### OpenAI (Direct)
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
uv pip install longcache[openai]
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
```python
|
|
465
|
+
from openai import OpenAI
|
|
466
|
+
from longcache.integrations.openai import CachedOpenAI
|
|
467
|
+
|
|
468
|
+
client = CachedOpenAI(OpenAI(api_key="sk-..."), cache=LongCache())
|
|
469
|
+
response = client.chat.completions.create(
|
|
470
|
+
model="gpt-4o",
|
|
471
|
+
messages=[{"role": "user", "content": "What is RAG?"}],
|
|
472
|
+
)
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## FastAPI Middleware
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
uv pip install longcache[server]
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
```python
|
|
484
|
+
from fastapi import FastAPI
|
|
485
|
+
from longcache import LongCache
|
|
486
|
+
from longcache.middleware import LongCacheMiddleware
|
|
487
|
+
|
|
488
|
+
app = FastAPI()
|
|
489
|
+
app.add_middleware(
|
|
490
|
+
LongCacheMiddleware,
|
|
491
|
+
cache=LongCache(),
|
|
492
|
+
cached_routes={"/v1/chat/completions"},
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
# All POST requests to /v1/chat/completions are now semantically cached.
|
|
496
|
+
# X-LongCache: HIT โ cached response served
|
|
497
|
+
# X-LongCache: (absent) โ fresh LLM response
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Streaming Cache Adapter
|
|
501
|
+
|
|
502
|
+
```python
|
|
503
|
+
from longcache.middleware import StreamingCacheAdapter
|
|
504
|
+
|
|
505
|
+
def my_stream(query: str):
|
|
506
|
+
for chunk in llm.stream(query):
|
|
507
|
+
yield chunk
|
|
508
|
+
|
|
509
|
+
adapter = StreamingCacheAdapter(cache, my_stream)
|
|
510
|
+
|
|
511
|
+
# MISS โ streams from LLM, saves full response in cache
|
|
512
|
+
for token in adapter.stream("What is RAG?"):
|
|
513
|
+
print(token, end="")
|
|
514
|
+
|
|
515
|
+
# HIT โ replays cached response with streaming pacing
|
|
516
|
+
for token in adapter.stream("What is RAG?"):
|
|
517
|
+
print(token, end="")
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## CLI
|
|
523
|
+
|
|
524
|
+
LongCache ships a command-line tool for cache management and inspection:
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
# View cache statistics (hits, misses, hit rate, estimated savings)
|
|
528
|
+
longcache stats
|
|
529
|
+
longcache stats --namespace faq
|
|
530
|
+
|
|
531
|
+
# Clear a namespace
|
|
532
|
+
longcache clear --namespace faq --yes
|
|
533
|
+
|
|
534
|
+
# Clear the entire cache
|
|
535
|
+
longcache clear --all --yes
|
|
536
|
+
|
|
537
|
+
# Inspect the nearest cached entry for a query
|
|
538
|
+
longcache inspect --query "refund policy"
|
|
539
|
+
|
|
540
|
+
# Export cache entries as JSON
|
|
541
|
+
longcache export --format json
|
|
542
|
+
longcache export --namespace faq -o faq_cache.json
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## API Reference
|
|
548
|
+
|
|
549
|
+
### `LongCache(config=None)`
|
|
550
|
+
|
|
551
|
+
Main facade class. Accepts an optional `CacheConfig`; uses production-safe defaults otherwise.
|
|
552
|
+
|
|
553
|
+
#### Core Methods
|
|
554
|
+
|
|
555
|
+
| Method | Description |
|
|
556
|
+
|--------|-------------|
|
|
557
|
+
| `get(query, namespace, model, system_prompt, temperature)` | Check cache. Returns cached response or `None`. Increments miss count on MISS. |
|
|
558
|
+
| `set(query, response, namespace, ttl, tags, model, system_prompt, temperature)` | Store a query-response pair with optional tags and context scope. |
|
|
559
|
+
| `aget(...)` | Async version of `get()`. Non-blocking. |
|
|
560
|
+
| `aset(...)` | Async version of `set()`. Non-blocking. |
|
|
561
|
+
| `cached(func, *, namespace, ttl, tags, model, system_prompt, temperature)` | Decorator for sync **and** async functions. |
|
|
562
|
+
| `cached_stream(func, *, namespace, ttl, tags, model, system_prompt, temperature)` | Decorator for sync/async **generator** functions (streaming LLMs). |
|
|
563
|
+
| `invalidate(namespace, model, system_prompt, temperature)` | Delete all entries in a namespace. Returns count deleted. |
|
|
564
|
+
| `invalidate_by_tag(tag, namespace, ...)` | Delete all entries carrying a given tag. Returns count deleted. |
|
|
565
|
+
| `clear()` | Clear the entire cache across all namespaces. Returns count. |
|
|
566
|
+
| `stats()` | Returns hit rate, miss count, estimated tokens saved, estimated cost saved, and total entries. |
|
|
567
|
+
| `inspect(query, namespace, ...)` | Find nearest cached entry without returning it. Useful for debugging. |
|
|
568
|
+
| `record_hit()` | Manually record a cache hit (thread-safe). For custom integrations. |
|
|
569
|
+
| `record_miss(elapsed_ms)` | Manually record a cache miss with elapsed generation time (thread-safe). |
|
|
570
|
+
|
|
571
|
+
#### `@cache.cached` Decorator Options
|
|
572
|
+
|
|
573
|
+
```python
|
|
574
|
+
@cache.cached(
|
|
575
|
+
namespace="faq", # Logical isolation group
|
|
576
|
+
ttl=7200, # Entry lifetime override (seconds)
|
|
577
|
+
tags=["billing"], # Tags for selective invalidation
|
|
578
|
+
model="gpt-4o", # Scopes entries to this model
|
|
579
|
+
system_prompt="...", # Scopes entries to this system prompt
|
|
580
|
+
temperature=0.0, # Scopes entries to this temperature
|
|
581
|
+
)
|
|
582
|
+
def my_chain(query: str) -> str:
|
|
583
|
+
...
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
Works identically for `@cache.cached_stream`.
|
|
587
|
+
|
|
588
|
+
#### `stats()` Return Value
|
|
589
|
+
|
|
590
|
+
```python
|
|
591
|
+
{
|
|
592
|
+
"hits": 420,
|
|
593
|
+
"misses": 180,
|
|
594
|
+
"hit_rate": 0.70,
|
|
595
|
+
"total_saved_ms": 840_000.0,
|
|
596
|
+
"estimated_tokens_saved": 630_000,
|
|
597
|
+
"estimated_cost_saved_usd": 18.90,
|
|
598
|
+
"total_entries": 95,
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Statistics are protected by a reentrant threading lock and persisted to the SQLite backend โ they survive process restarts.
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
### `CacheConfig` Field Reference
|
|
607
|
+
|
|
608
|
+
| Field | Type | Default | Description |
|
|
609
|
+
|-------|------|---------|-------------|
|
|
610
|
+
| `similarity_threshold` | `float` | `0.92` | Cosine similarity threshold for cache hits (0.0โ1.0) |
|
|
611
|
+
| `ttl_seconds` | `int` | `3600` | Entry lifetime in seconds. `0` = never expire |
|
|
612
|
+
| `max_entries` | `int` | `10_000` | Max entries per namespace (LRU eviction) |
|
|
613
|
+
| `namespace` | `str` | `"default"` | Default namespace |
|
|
614
|
+
| `embedder_model` | `str` | `"BAAI/bge-small-en-v1.5"` | Local sentence-transformers model |
|
|
615
|
+
| `backend` | `str` | `"sqlite"` | `"sqlite"`, `"redis"`, `"async_redis"`, `"memory"` |
|
|
616
|
+
| `db_path` | `str` | `".longcache/cache.db"` | SQLite database path |
|
|
617
|
+
| `redis_url` | `str` | `"redis://localhost:6379/0"` | Redis connection URL |
|
|
618
|
+
| `track_stats` | `bool` | `True` | Track hit/miss stats (thread-safe, persisted in SQLite) |
|
|
619
|
+
| `faiss_index_type` | `str` | `"flat"` | `"flat"` (exact) or `"hnsw"` (approximate) |
|
|
620
|
+
| `hnsw_m` | `int` | `32` | HNSW connections per node (higher = more accurate, more memory) |
|
|
621
|
+
| `hnsw_ef_search` | `int` | `64` | HNSW search depth (higher = more accurate, slower) |
|
|
622
|
+
| `model` | `str` | `""` | Global model identifier for composite key scoping |
|
|
623
|
+
| `system_prompt` | `str` | `""` | Global system prompt for composite key scoping |
|
|
624
|
+
| `temperature` | `float` | `-1.0` | Global temperature for composite key scoping (`-1.0` = unset) |
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
### `CacheEntry` Field Reference
|
|
629
|
+
|
|
630
|
+
Returned by `inspect()` and used internally by all backends.
|
|
631
|
+
|
|
632
|
+
| Field | Type | Description |
|
|
633
|
+
|-------|------|-------------|
|
|
634
|
+
| `id` | `int \| None` | Backend-assigned unique identifier |
|
|
635
|
+
| `namespace` | `str` | Effective namespace (includes composite key suffix if set) |
|
|
636
|
+
| `query` | `str` | Original query text |
|
|
637
|
+
| `vector` | `np.ndarray` | 384-dim L2-normalized float32 embedding |
|
|
638
|
+
| `response` | `Any` | Cached LLM response (must be JSON-serializable) |
|
|
639
|
+
| `created_at` | `float` | Unix timestamp of entry creation |
|
|
640
|
+
| `expires_at` | `float \| None` | Unix timestamp of expiry (`None` = no expiry) |
|
|
641
|
+
| `hit_count` | `int` | Number of times this entry has been served |
|
|
642
|
+
| `model` | `str` | Model identifier at write time |
|
|
643
|
+
| `system_prompt` | `str` | System prompt at write time |
|
|
644
|
+
| `temperature` | `float` | Temperature at write time (`-1.0` = unset) |
|
|
645
|
+
| `tags` | `list[str]` | Tags for selective invalidation |
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
## Advanced Usage
|
|
650
|
+
|
|
651
|
+
### Per-Namespace Threshold Tuning
|
|
652
|
+
|
|
653
|
+
```python
|
|
654
|
+
faq_cache = LongCache(CacheConfig(namespace="faq", similarity_threshold=0.95))
|
|
655
|
+
chat_cache = LongCache(CacheConfig(namespace="general", similarity_threshold=0.85))
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
### Per-Entry TTL Override
|
|
659
|
+
|
|
660
|
+
```python
|
|
661
|
+
cache.set("volatile_query", response, ttl=300) # 5-minute TTL
|
|
662
|
+
cache.set("stable_query", response, ttl=86400) # 24-hour TTL
|
|
663
|
+
cache.set("permanent", response, ttl=0) # Never expires
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Cache Invalidation Patterns
|
|
667
|
+
|
|
668
|
+
```python
|
|
669
|
+
# Clear a specific namespace
|
|
670
|
+
cache.invalidate("faq")
|
|
671
|
+
|
|
672
|
+
# Clear entries scoped to a specific model
|
|
673
|
+
cache.invalidate("faq", model="gpt-4o")
|
|
674
|
+
|
|
675
|
+
# Invalidate by tag (e.g., after a document update)
|
|
676
|
+
cache.invalidate_by_tag("doc:refund_v2")
|
|
677
|
+
|
|
678
|
+
# Nuclear option โ clear everything
|
|
679
|
+
cache.clear()
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### Async Manual Control
|
|
683
|
+
|
|
684
|
+
```python
|
|
685
|
+
cached = await cache.aget("What is your refund policy?")
|
|
686
|
+
if cached is None:
|
|
687
|
+
result = await my_pipeline("What is your refund policy?")
|
|
688
|
+
await cache.aset("What is your refund policy?", result, tags=["policy"])
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
### HNSW Index for Large Caches (100K+ Entries)
|
|
692
|
+
|
|
693
|
+
```python
|
|
694
|
+
cache = LongCache(CacheConfig(
|
|
695
|
+
faiss_index_type="hnsw",
|
|
696
|
+
hnsw_m=32, # More connections โ more accurate, more memory
|
|
697
|
+
hnsw_ef_search=64, # Higher โ more accurate, slower
|
|
698
|
+
max_entries=500_000,
|
|
699
|
+
))
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
### Inspecting the Cache
|
|
703
|
+
|
|
704
|
+
```python
|
|
705
|
+
entry = cache.inspect("refund policy")
|
|
706
|
+
# {
|
|
707
|
+
# 'id': 42,
|
|
708
|
+
# 'query': 'What is your refund policy?',
|
|
709
|
+
# 'namespace': 'default',
|
|
710
|
+
# 'hit_count': 7,
|
|
711
|
+
# 'tags': ['billing', 'policy'],
|
|
712
|
+
# 'model': 'gpt-4o',
|
|
713
|
+
# 'created_at': 1700000000.0,
|
|
714
|
+
# 'expires_at': None,
|
|
715
|
+
# 'is_expired': False,
|
|
716
|
+
# }
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### Custom Integration: Manual Hit/Miss Recording
|
|
720
|
+
|
|
721
|
+
For custom wrappers that call `get()`/`set()` manually:
|
|
722
|
+
|
|
723
|
+
```python
|
|
724
|
+
result = cache.get(query, track_miss=False)
|
|
725
|
+
if result is not None:
|
|
726
|
+
cache.record_hit()
|
|
727
|
+
return result
|
|
728
|
+
|
|
729
|
+
t0 = time.time()
|
|
730
|
+
result = my_pipeline(query)
|
|
731
|
+
cache.record_miss(elapsed_ms=(time.time() - t0) * 1000)
|
|
732
|
+
cache.set(query, result)
|
|
733
|
+
return result
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
---
|
|
737
|
+
|
|
738
|
+
## Architecture
|
|
739
|
+
|
|
740
|
+
### Component Overview
|
|
741
|
+
|
|
742
|
+
```
|
|
743
|
+
src/longcache/
|
|
744
|
+
โโโ __init__.py # Public API: LongCache, CacheConfig
|
|
745
|
+
โโโ config.py # CacheConfig dataclass + from_dict/from_env
|
|
746
|
+
โโโ cli.py # CLI: stats, clear, inspect, export
|
|
747
|
+
โโโ middleware.py # FastAPI/Starlette ASGI middleware + StreamingCacheAdapter
|
|
748
|
+
โโโ core/
|
|
749
|
+
โ โโโ entry.py # CacheEntry dataclass (query, vector, response, tags, context)
|
|
750
|
+
โ โโโ embedder.py # LocalEmbedder โ BGE-small singleton, lazy-loaded
|
|
751
|
+
โ โโโ index.py # SimilarityIndex โ FAISS Flat/HNSW, rebuild-safe
|
|
752
|
+
โ โโโ cache.py # LongCache โ main orchestrator, thread-safe stats
|
|
753
|
+
โโโ backends/
|
|
754
|
+
โ โโโ base.py # AbstractCacheBackend ABC
|
|
755
|
+
โ โโโ sqlite.py # SQLiteCacheBackend โ WAL mode, tag table, stats persistence
|
|
756
|
+
โ โโโ redis.py # RedisCacheBackend โ sync, tag support, atomic TTL
|
|
757
|
+
โ โโโ async_redis.py # AsyncRedisCacheBackend โ async, non-blocking
|
|
758
|
+
โ โโโ memory.py # InMemoryCacheBackend โ testing, zero I/O
|
|
759
|
+
โโโ integrations/
|
|
760
|
+
โโโ langchain.py # CachedLLM, CachedChain
|
|
761
|
+
โโโ langgraph.py # CachedState
|
|
762
|
+
โโโ llamaindex.py # CachedQueryEngine
|
|
763
|
+
โโโ openai.py # CachedOpenAI
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
### Embedding Model
|
|
767
|
+
|
|
768
|
+
LongCache uses **BAAI/bge-small-en-v1.5** by default:
|
|
769
|
+
|
|
770
|
+
- **384-dimensional** vectors โ compact and fast
|
|
771
|
+
- **Sub-5ms** embedding on modern CPU hardware
|
|
772
|
+
- **Fully offline** after initial weight download โ no API calls ever
|
|
773
|
+
- **L2-normalized** โ inner product equals cosine similarity in FAISS
|
|
774
|
+
- **Singleton pattern** โ the model is loaded once and reused across all instances
|
|
775
|
+
|
|
776
|
+
### FAISS Index
|
|
777
|
+
|
|
778
|
+
| Index | Config Value | Characteristics |
|
|
779
|
+
|-------|-------------|-----------------|
|
|
780
|
+
| `IndexFlatIP` | `"flat"` | Exact search, deterministic, best for โค 100K entries |
|
|
781
|
+
| `IndexHNSWFlat` | `"hnsw"` | Approximate search, sub-ms at any scale, best for 100K+ entries |
|
|
782
|
+
|
|
783
|
+
The FAISS index is **rebuild-safe**: after TTL sweeps, namespace invalidations, or evictions, the index is fully rebuilt from the backend rather than incrementally patched โ preventing stale vector accumulation.
|
|
784
|
+
|
|
785
|
+
### Thread Safety
|
|
786
|
+
|
|
787
|
+
- **Stats** (`hits`, `misses`, `total_saved_ms`) are protected by a dedicated `threading.Lock()`.
|
|
788
|
+
- **SQLite backend** uses WAL mode and a per-connection `threading.RLock()`.
|
|
789
|
+
- **The TTL sweeper** runs as a daemon thread and signals shutdown cleanly via `threading.Event`.
|
|
790
|
+
- **`get()`/`aget()`** correctly record misses in a thread-safe manner.
|
|
791
|
+
|
|
792
|
+
### Background TTL Sweeper
|
|
793
|
+
|
|
794
|
+
A lightweight daemon thread runs every 5 minutes to delete expired entries and rebuild the FAISS index. Starts automatically when `ttl_seconds > 0`. Shuts down gracefully when the `LongCache` instance is garbage-collected.
|
|
795
|
+
|
|
796
|
+
### Persistent Statistics
|
|
797
|
+
|
|
798
|
+
Hit/miss statistics are persisted to the SQLite backend and survive process restarts. All stat mutations go through a reentrant lock to prevent race conditions under concurrent load.
|
|
799
|
+
|
|
800
|
+
---
|
|
801
|
+
|
|
802
|
+
## Performance
|
|
803
|
+
|
|
804
|
+
### Expected Cache Hit Rates
|
|
805
|
+
|
|
806
|
+
| Scenario | Hit Rate | Cost Savings |
|
|
807
|
+
|----------|----------|--------------|
|
|
808
|
+
| FAQ chatbot | 60โ80% | 40โ60% |
|
|
809
|
+
| Customer support | 40โ60% | 25โ40% |
|
|
810
|
+
| Internal knowledge base | 50โ70% | 30โ50% |
|
|
811
|
+
| General RAG pipeline | 30โ50% | 20โ35% |
|
|
812
|
+
|
|
813
|
+
### Latency Comparison
|
|
814
|
+
|
|
815
|
+
| Operation | Typical Latency |
|
|
816
|
+
|-----------|----------------|
|
|
817
|
+
| Full RAG pipeline (retrieval + LLM) | 1,000โ3,000 ms |
|
|
818
|
+
| LongCache HIT (embed + FAISS search) | 5โ15 ms |
|
|
819
|
+
| LongCache MISS overhead | 5โ10 ms |
|
|
820
|
+
|
|
821
|
+
---
|
|
822
|
+
|
|
823
|
+
## Project Structure
|
|
824
|
+
|
|
825
|
+
```
|
|
826
|
+
longcache/
|
|
827
|
+
โโโ pyproject.toml
|
|
828
|
+
โโโ README.md
|
|
829
|
+
โโโ src/
|
|
830
|
+
โ โโโ longcache/
|
|
831
|
+
โ โโโ __init__.py
|
|
832
|
+
โ โโโ config.py
|
|
833
|
+
โ โโโ cli.py
|
|
834
|
+
โ โโโ middleware.py
|
|
835
|
+
โ โโโ core/
|
|
836
|
+
โ โ โโโ __init__.py
|
|
837
|
+
โ โ โโโ entry.py
|
|
838
|
+
โ โ โโโ embedder.py
|
|
839
|
+
โ โ โโโ index.py
|
|
840
|
+
โ โ โโโ cache.py
|
|
841
|
+
โ โโโ backends/
|
|
842
|
+
โ โ โโโ __init__.py
|
|
843
|
+
โ โ โโโ base.py
|
|
844
|
+
โ โ โโโ sqlite.py
|
|
845
|
+
โ โ โโโ redis.py
|
|
846
|
+
โ โ โโโ async_redis.py
|
|
847
|
+
โ โ โโโ memory.py
|
|
848
|
+
โ โโโ integrations/
|
|
849
|
+
โ โโโ __init__.py
|
|
850
|
+
โ โโโ langchain.py
|
|
851
|
+
โ โโโ langgraph.py
|
|
852
|
+
โ โโโ llamaindex.py
|
|
853
|
+
โ โโโ openai.py
|
|
854
|
+
โโโ tests/
|
|
855
|
+
โ โโโ conftest.py
|
|
856
|
+
โ โโโ test_embedder.py
|
|
857
|
+
โ โโโ test_index.py
|
|
858
|
+
โ โโโ test_cache.py
|
|
859
|
+
โ โโโ test_backends.py
|
|
860
|
+
โ โโโ test_langchain.py
|
|
861
|
+
โ โโโ test_middleware.py
|
|
862
|
+
โ โโโ test_cli.py
|
|
863
|
+
โโโ examples/
|
|
864
|
+
โโโ basic_langchain.py
|
|
865
|
+
โโโ fastapi_middleware.py
|
|
866
|
+
โโโ redis_production.py
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
---
|
|
870
|
+
|
|
871
|
+
## Dependencies
|
|
872
|
+
|
|
873
|
+
### Core (Required)
|
|
874
|
+
|
|
875
|
+
| Package | Version | Purpose |
|
|
876
|
+
|---------|---------|---------|
|
|
877
|
+
| `sentence-transformers` | `>=2.3.0` | Local embedding โ BGE-small |
|
|
878
|
+
| `faiss-cpu` | `>=1.7.4` | Similarity search (Flat & HNSW) |
|
|
879
|
+
| `numpy` | `>=1.24.0,<2.0.0` | Vector math |
|
|
880
|
+
| `torch` | CPU (via uv) | sentence-transformers runtime |
|
|
881
|
+
| `click` | `>=8.1.0` | CLI framework |
|
|
882
|
+
|
|
883
|
+
> **Note**: `numpy<2.0.0` is required โ numpy 2.0 breaks ABI compatibility with `faiss-cpu` and `torch`.
|
|
884
|
+
|
|
885
|
+
### Optional Extras
|
|
886
|
+
|
|
887
|
+
| Extra | Package(s) | Purpose |
|
|
888
|
+
|-------|-----------|---------|
|
|
889
|
+
| `[redis]` | `redis>=5.0.0,<6.0.0` | Redis & async Redis backends |
|
|
890
|
+
| `[langchain]` | `langchain-core>=1.0.0`, `langgraph>=1.0.0` | LangChain + LangGraph |
|
|
891
|
+
| `[llamaindex]` | `llama-index-core>=0.10.0` | LlamaIndex integration |
|
|
892
|
+
| `[openai]` | `openai>=1.0.0,<2.0.0` | Direct OpenAI integration |
|
|
893
|
+
| `[server]` | `fastapi>=0.109.0`, `starlette>=0.36.0`, `uvicorn>=0.27.0` | ASGI middleware |
|
|
894
|
+
| `[all]` | All of the above | Everything |
|
|
895
|
+
|
|
896
|
+
### Development
|
|
897
|
+
|
|
898
|
+
| Package | Version | Purpose |
|
|
899
|
+
|---------|---------|---------|
|
|
900
|
+
| `pytest` | `>=8.0.0` | Test runner |
|
|
901
|
+
| `pytest-asyncio` | `>=0.23.0` | Async test support |
|
|
902
|
+
| `pytest-cov` | `>=5.0.0` | Coverage reporting |
|
|
903
|
+
| `httpx` | `>=0.27.0` | ASGI test client |
|
|
904
|
+
| `ruff` | `>=0.4.0` | Linter / formatter |
|
|
905
|
+
|
|
906
|
+
---
|
|
907
|
+
|
|
908
|
+
## Running Tests
|
|
909
|
+
|
|
910
|
+
```bash
|
|
911
|
+
# Install in editable mode with dev dependencies
|
|
912
|
+
uv pip install -e ".[dev]"
|
|
913
|
+
|
|
914
|
+
# Run all tests
|
|
915
|
+
uv run pytest
|
|
916
|
+
|
|
917
|
+
# Run with coverage report
|
|
918
|
+
uv run pytest --cov=longcache --cov-report=term-missing
|
|
919
|
+
|
|
920
|
+
# Run a specific test file
|
|
921
|
+
uv run pytest tests/test_cache.py
|
|
922
|
+
|
|
923
|
+
# Verbose output
|
|
924
|
+
uv run pytest -v
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
---
|
|
928
|
+
|
|
929
|
+
## Contributing & Community
|
|
930
|
+
|
|
931
|
+
We welcome contributions! Please see:
|
|
932
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md) โ Setup, testing, and pull request guidelines.
|
|
933
|
+
- [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) โ Contributor Covenant v2.1 standards.
|
|
934
|
+
- [SECURITY.md](SECURITY.md) โ Vulnerability reporting and coordinated disclosure.
|
|
935
|
+
|
|
936
|
+
---
|
|
937
|
+
|
|
938
|
+
## License
|
|
939
|
+
|
|
940
|
+
This project is licensed under the MIT License โ see the [LICENSE](LICENSE) file for details.
|
|
941
|
+
|
|
942
|
+
Copyright (c) 2026 ENDEVSOLS
|