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.
@@ -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> &nbsp;ยท&nbsp;
83
+ <a href="https://endevsols.github.io/LongCache/getting-started/quickstart/"><strong>Quick Start</strong></a> &nbsp;ยท&nbsp;
84
+ <a href="https://endevsols.github.io/LongCache/api-reference/"><strong>API Reference</strong></a> &nbsp;ยท&nbsp;
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