ragdedup
A lightweight semantic cache for LLM and RAG pipelines. One class, one method, zero required dependencies.
ragdedup recognizes when a new request means roughly the same thing as one you already answered, even if the wording is different, and returns the cached response instead of calling the model again. Wrap any embedding model and any vector store behind a single call:
result = cache.get_or_compute(text, llm_fn)
It embeds text, checks similarity against everything already cached, and either returns the cached response or calls llm_fn(text), stores the result, and returns that instead. This is the pattern behind cutting manual email handling time by roughly 75 percent in a production lead generation pipeline: near duplicate messages like "any updates?", "just checking in", and "following up on this" stopped triggering a fresh LLM call every single time.
Why this exists, and where the real competition is
There is already a mature, actively maintained project doing semantic caching for LLMs: GPTCache (by Zilliz). It is the right call if you want maximum configurability.
| Tool | What it actually does | How ragdedup compares |
|---|---|---|
gptcache |
Full featured semantic cache. Supports many vector stores and cache backends (Milvus, Redis, SQLite, and others), configurable similarity evaluators, TTL and LRU eviction, and deep integration with LangChain and LlamaIndex. | The more complete option, and the right choice if you want that breadth. Getting there means wiring up a cache_manager, embedding_func, and similarity_evaluation pipeline. ragdedup is one class and one method, trading configurability for a much smaller footprint. |
mail-deduplicate, dedupe |
Deduplicate raw email files or structured database records using header/field matching or entity resolution. | Built for files and databases, not for wrapping a live LLM call. No embedding based text similarity, no get_or_compute style API. |
diskcache, joblib.Memory, LangChain's InMemoryCache |
General purpose result caching, keyed by exact input match. | Exact match only. "Any updates?" and "just checking in" are different cache keys to these tools, so a near duplicate message still triggers a fresh LLM call. |
What ragdedup actually offers that is worth knowing about:
- Zero required dependencies. The default in-memory backend is pure Python cosine similarity, no numpy needed. Add
openai,sentence-transformers, orfaissonly when you actually want them. - One class, one method.
SemanticCache(embedder, vector_store).get_or_compute(text, llm_fn). Nocache_managerorsimilarity_evaluationobjects to assemble first. - Sync and async from day one.
get_or_computeandaget_or_computeboth ship in v0.1.0, andaget_or_computeaccepts either a sync or an asyncllm_fn. - Built in cost reporting.
stats.estimated_cost_saved(cost_per_call)gives you a dollar figure you can put directly into a report. - Plugs straight into other LLMClient style interfaces. Since
llm_fnis justCallable[[str], str], you can pass any object's.completemethod in directly, includingagentic_rag_toolkit'sLLMClient.
If you need multi-backend flexibility, a large existing community, or out of the box LangChain and LlamaIndex hooks, use GPTCache. Use ragdedup when you want the core idea in the smallest possible footprint.
Install
pip install ragdedup # core only, zero extra dependencies
pip install "ragdedup[openai]" # + real OpenAI embeddings
pip install "ragdedup[sentence-transformers]" # + free local embeddings
pip install "ragdedup[faiss]" # + FAISS backed vector store for scale
Quickstart
from ragdedup import SemanticCache, HashEmbedder
def my_llm(text: str) -> str:
... # call your real model here, return the text response
cache = SemanticCache(embedder=HashEmbedder(dim=64), similarity_threshold=0.85)
result = cache.get_or_compute("Any updates on my order?", my_llm)
print(result.was_cached, result.response)
result = cache.get_or_compute("Just checking in on my order", my_llm)
print(result.was_cached) # True, second call is a semantic match, no LLM call made
print(cache.stats.hit_rate, cache.stats.estimated_cost_saved(cost_per_call=0.002))
HashEmbedder is dependency free and deterministic, good for trying the package out or for tests, but it is not a real semantic model. Swap in OpenAIEmbedder or SentenceTransformerEmbedder for production use. See examples/basic_usage.py (no API keys needed) and examples/openai_faiss_example.py (production shaped, real OpenAI embeddings plus FAISS).
API Reference
SemanticCache, CacheResult, CacheStats (module: ragdedup.cache)
Constructor
SemanticCache(
embedder: Embedder,
vector_store: VectorStore | None = None,
similarity_threshold: float = 0.92,
)
| Argument | Type | Default | Notes |
|---|---|---|---|
embedder |
Embedder |
required | Any object with .embed(text) -> list[float] |
vector_store |
VectorStore or None |
InMemoryVectorStore() |
Any object with .add(), .query(), .size(), .clear() |
similarity_threshold |
float |
0.92 |
Must be between 0.0 and 1.0, raises ValueError otherwise. Higher means stricter matching (fewer false cache hits, more LLM calls); lower means looser matching (more cache hits, higher risk of returning a response for a question that was not quite the same) |
Methods
.get_or_compute(text: str, llm_fn: Callable[[str], str]) -> CacheResult
Raises ValueError if text is empty or blank. Embeds text, checks the vector store for a match at or above similarity_threshold. On a match, returns the cached response without calling llm_fn. On no match, calls llm_fn(text), stores the result, and returns it.
.aget_or_compute(text: str, llm_fn) -> CacheResult
Same behavior, async. llm_fn may be a regular function or an async def function; both are detected and handled automatically.
.clear(reset_stats: bool = False) -> None
Empties the vector store. Pass reset_stats=True to also zero out .stats.
.size() -> int
Number of entries currently cached.
CacheResult fields: response: str, was_cached: bool, similarity: float | None (the match score, None on a miss), matched_text: str | None (the original text that matched, None on a miss).
CacheStats (available as cache.stats): hits: int, misses: int, total (property), hit_rate (property, hits / total), and estimated_cost_saved(cost_per_call: float) -> float.
from ragdedup import SemanticCache, HashEmbedder
cache = SemanticCache(embedder=HashEmbedder(), similarity_threshold=0.9)
result = cache.get_or_compute("what is your refund policy", my_llm)
print(result.response, result.was_cached, result.similarity)
print(cache.stats.hits, cache.stats.hit_rate, cache.stats.estimated_cost_saved(0.002))
Embedder (Protocol), HashEmbedder, OpenAIEmbedder, SentenceTransformerEmbedder (module: ragdedup.embedders)
Any object with .embed(text: str) -> list[float] satisfies Embedder automatically, no subclassing needed.
| Class | Constructor args | Requires | Notes |
|---|---|---|---|
HashEmbedder |
dim: int = 64 |
nothing | Deterministic bag of hashed words. Fine for demos and tests, not a real semantic model |
OpenAIEmbedder |
model: str = "text-embedding-3-small" |
pip install openai + OPENAI_API_KEY |
Real semantic embeddings, 1536 dimensions for the default model |
SentenceTransformerEmbedder |
model_name: str = "all-MiniLM-L6-v2" |
pip install sentence-transformers |
Real semantic embeddings, free, local, 384 dimensions for the default model |
from ragdedup import OpenAIEmbedder
embedder = OpenAIEmbedder(model="text-embedding-3-small")
vector = embedder.embed("hello world")
VectorStore (Protocol), InMemoryVectorStore, FAISSVectorStore, StoreMatch, cosine_similarity (module: ragdedup.stores)
Any object with .add(), .query(), .size(), .clear() satisfies VectorStore automatically.
InMemoryVectorStore(max_size: int | None = 10_000, ttl_seconds: float | None = None)
Thread safe, dependency free. max_size evicts the oldest entry first once exceeded (this is what makes the cache "rolling"). ttl_seconds expires entries older than that many seconds, checked lazily on the next query. Both raise ValueError if set to a non-positive number. Pass None to disable either limit.
FAISSVectorStore(dim: int)
Requires pip install faiss-cpu. Faster and able to hold far more entries than InMemoryVectorStore. Known limitation: FAISS's flat index does not support deleting individual entries cheaply, so this store has no max_size or ttl_seconds and grows unbounded. Monitor .size() yourself at real scale, or wrap your own Chroma/Pinecone client the same way (see examples/) if you need automatic expiry.
store = InMemoryVectorStore(max_size=5000, ttl_seconds=86400) # rolling 24 hour cache, max 5000 entries
Methods on both: .add(id, vector, text, metadata=None), .query(vector, top_k=1) -> list[StoreMatch], .size() -> int, .clear() -> None.
StoreMatch fields: id: str, score: float, text: str, metadata: dict.
cosine_similarity(a: list[float], b: list[float]) -> float is the plain function InMemoryVectorStore uses internally, exported in case you want it directly. Raises ValueError if the two vectors are different lengths.
Known limitations
Under concurrent access, two near simultaneous calls with the same brand new (not yet cached) text can both miss the cache and both call llm_fn once, before either result gets stored. This does not cause incorrect behavior, it is just an occasional missed cache hit under heavy concurrent load on genuinely new queries. If you need strict single flight behavior (guarantee llm_fn runs at most once per unique input, even under a race), add your own per key lock in front of get_or_compute.
FAISSVectorStore has no automatic eviction, see above.
HashEmbedder is not a real semantic model. If you use it in production instead of just for demos and tests, you will get false cache hits and misses that a real embedding model would not.
Development
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pytest --cov=ragdedup tests/ -v
License
MIT, see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ragdedup-0.1.0.tar.gz.
File metadata
- Download URL: ragdedup-0.1.0.tar.gz
- Upload date:
- Size: 14.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bfb8034bf460a15c1fa1a4186844e58db8335b1d0485a218611c38d543d17a02
|
|
| MD5 |
6984b92763ad52051943eb902a8e425c
|
|
| BLAKE2b-256 |
8fe7704b175168d62b150fc5de203c5d7e4e2006aebdced372acdda836ef56d4
|
Provenance
The following attestation bundles were made for ragdedup-0.1.0.tar.gz:
Publisher:
publish.yml on Mohanapriya-sk/ragdedup
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ragdedup-0.1.0.tar.gz -
Subject digest:
bfb8034bf460a15c1fa1a4186844e58db8335b1d0485a218611c38d543d17a02 - Sigstore transparency entry: 2226247234
- Sigstore integration time:
-
Permalink:
Mohanapriya-sk/ragdedup@d24a4b704737c9f8b84f3eb29c88504a302f885d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Mohanapriya-sk
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d24a4b704737c9f8b84f3eb29c88504a302f885d -
Trigger Event:
push
-
Statement type:
File details
Details for the file ragdedup-0.1.0-py3-none-any.whl.
File metadata
- Download URL: ragdedup-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d4be345860f44171a07e497147faf75685ad07fe38bca71642c50a8b5f9acc3
|
|
| MD5 |
cbf23fb097f234f38db80ba3cc581766
|
|
| BLAKE2b-256 |
faab0e09219b19af6b4f78afa67483aa838acb650c82c1e13783d23c0ee3990e
|
Provenance
The following attestation bundles were made for ragdedup-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on Mohanapriya-sk/ragdedup
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ragdedup-0.1.0-py3-none-any.whl -
Subject digest:
1d4be345860f44171a07e497147faf75685ad07fe38bca71642c50a8b5f9acc3 - Sigstore transparency entry: 2226247260
- Sigstore integration time:
-
Permalink:
Mohanapriya-sk/ragdedup@d24a4b704737c9f8b84f3eb29c88504a302f885d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Mohanapriya-sk
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d24a4b704737c9f8b84f3eb29c88504a302f885d -
Trigger Event:
push
-
Statement type: