Skip to main content

memory-reuse

CI PyPI Python License: MIT Code style: black

An execution cache layer for AI agents that cuts LLM and tool call costs by avoiding redundant computation. Drop it into any Python agent or LangGraph workflow with a single decorator.

  • Framework-agnostic — LangGraph, LiteLLM, or any plain Python function.
  • Zero required dependencies — the core runs on the standard library alone.
  • Safe by default — per-user / per-session scoping prevents cross-user cache leaks.
  • Typed — ships with py.typed, fully type-hinted.

How it works

When your agent calls an LLM or a tool, memory-reuse hashes the inputs and checks the cache first. On a hit it returns the stored result instantly — no tokens spent, no API call made. On a miss it runs the real call and stores the result for next time.

request ──► hash inputs ──► cache lookup
                              ├── HIT  ──► return cached result (0 cost)
                              └── MISS ──► run LLM/tool ──► store ──► return

Exact vs semantic: by default memory-reuse does exact-match caching — identical inputs hit the cache. Enabling the optional semantic cache also serves cached results for similar-but-not-identical inputs (reworded questions) using embedding similarity.


Install

Works with both pip and uv — pick whichever you use.

pip

pip install memory-reuse

uv

uv add memory-reuse

Optional extras

Extra What it adds pip uv
redis Redis backend support pip install memory-reuse[redis] uv add memory-reuse[redis]
litellm LiteLLM cached wrappers pip install memory-reuse[litellm] uv add memory-reuse[litellm]
semantic Semantic cache with API embeddings (OpenAI / LiteLLM) — no torch pip install memory-reuse[semantic] uv add memory-reuse[semantic]
semantic-local Semantic cache with local embeddings (sentence-transformers, pulls in torch) pip install memory-reuse[semantic-local] uv add memory-reuse[semantic-local]
all Everything above pip install memory-reuse[all] uv add memory-reuse[all]

Note: uv is a fast Python package manager. If you don't have it yet: pip install uv or see docs.astral.sh/uv


Quick start

from memory_reuse import MemoryCache, CacheConfig
from memory_reuse.integrations import cached_tool

cache = MemoryCache()                          # in-memory backend, 1-hour TTL

@cached_tool(cache, scope="global", ttl=300)   # cache for 5 minutes
async def search_web(query: str) -> list[str]:
    return await my_search_api(query)          # only called on cache miss

Usage patterns

1 — Basic exact cache (LLM responses)

from memory_reuse import MemoryCache

cache = MemoryCache()

# Manual get/set
result = await cache.exact.get(["gpt-4", prompt], scope="global", scope_id=None)
if result is None:
    result = await llm.ainvoke(prompt)
    await cache.exact.set(["gpt-4", prompt], result, scope="global",
                          scope_id=None, ttl=3600)

2 — LangGraph node caching

from memory_reuse.integrations import cached_node

@cached_node(cache, scope="user", key_fields=["messages"])
async def summarise(state: dict) -> dict:
    summary = await llm.ainvoke(state["messages"])
    return {"summary": summary}

The decorator reads user_id from the state dict automatically, or from cache.set_context(user_id=...).

3 — LangGraph tool caching

from memory_reuse.integrations import cached_tool

@cached_tool(cache, scope="session", ttl=120)
async def fetch_user_profile(user_id: str) -> dict:
    return await db.get_user(user_id)

4 — LiteLLM (works with OpenAI, Claude, Bedrock, Groq, Ollama, and 100+ more)

from memory_reuse.integrations import cached_litellm_completion, cached_litellm_embedding

# Completion — same prompt + model = cache hit, 0 tokens used
response = await cached_litellm_completion(
    cache,
    model="gpt-4o-mini",                  # swap for any LiteLLM model string
    messages=[{"role": "user", "content": "What is the capital of France?"}],
    ttl=3600,
    scope="global",
)

# Embeddings — deterministic, safe to cache for 24 hours
embeddings = await cached_litellm_embedding(
    cache,
    model="text-embedding-3-small",
    input=["What is machine learning?"],
)

Backend options

Backend Extra required Persistence Notes
memory none in-process only LRU eviction, TTL support
redis [redis] yes connection pool, lazy connect

Configure via code or environment variables:

export MEMORY_REUSE_BACKEND=redis
export MEMORY_REUSE_REDIS_URL=redis://localhost:6379/0
export MEMORY_REUSE_DEFAULT_TTL=600
export MEMORY_REUSE_DEFAULT_SCOPE=user
cache = MemoryCache.from_env()

Multi-scope support

cache.set_context(user_id="alice", session_id="sess-001")

# User-scoped: alice cannot see bob's cache
await cache.exact.get(["key"], scope="user", scope_id="alice")

# Session-scoped: isolated per conversation
await cache.tool.get("search", args, scope="session", scope_id="sess-001")

# Global: shared across all users — safe for public, stateless data
await cache.exact.get(["key"], scope="global", scope_id=None)

Using scope="user" without a user_id raises ScopeViolationError to prevent accidental cross-user data leaks.


Semantic cache

Exact caching only hits when inputs are identical. The semantic cache also serves a cached result when a new query is meaningfully similar to a stored one — so "What is 128 multiplied by 47?" can reuse the answer to "What is 128 times 47?". This lifts hit rates for natural-language workloads (chatbots, FAQ agents, docs Q&A) where the same intent is phrased many ways.

Choosing an embedding provider

The semantic cache turns text into vectors using one of three interchangeable providers, selected by embedding_provider:

Provider embedding_provider Runs Install Notes
OpenAI "openai" OpenAI API pip install memory-reuse[semantic] Hosted, paid per call. No torch.
LiteLLM "litellm" 100+ backends (Bedrock, Cohere, …) pip install memory-reuse[semantic] Model string picks the backend. No torch.
Local "local" Your machine see below sentence-transformers; private, no per-call cost; pulls in torch.

There are just two install commands to remember. The semantic extra covers both API providers (it bundles the small openai and litellm clients, plus numpy as a cosine-similarity speedup) and installs no torch:

pip install "memory-reuse[semantic]"     # OpenAI + LiteLLM embeddings, lightweight

Local embeddings need sentence-transformers, which depends on PyTorch. On a CPU-only machine (no NVIDIA GPU) install the CPU torch wheel first to avoid a ~2 GB GPU/CUDA download — the CPU build is ~200 MB:

pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install "memory-reuse[semantic-local]"

With a GPU you can skip the first line and just pip install "memory-reuse[semantic-local]". Either way, the model weights themselves (e.g. all-MiniLM-L6-v2, ~90 MB) download from Hugging Face on first use and are then cached on disk for offline reuse.

Quieter / fully offline runs. memory-reuse already silences the Hugging Face log chatter and the per-embedding progress bar. Once the model is cached, you can additionally skip Hugging Face's cache-validation HTTP checks by exporting HF_HUB_OFFLINE=1 (and TRANSFORMERS_OFFLINE=1) in your own process. Do this only in your application — set after the first (downloading) run, never inside a shared library.

Enabling it

Semantic caching is off by default — existing exact-only code is unchanged. Turn it on via CacheConfig by setting semantic_enabled=True and choosing an embedding provider (local, openai, or litellm):

from memory_reuse import MemoryCache, CacheConfig

cache = MemoryCache(CacheConfig(
    backend="memory",
    semantic_enabled=True,
    embedding_provider="local",              # local sentence-transformers model
    embedding_model="all-MiniLM-L6-v2",
    similarity_threshold=0.95,               # how close is "close enough"
))

# Exact-first, then semantic. An exact hit never computes an embedding.
result = await cache.lookup(
    ["qa", "What is 128 multiplied by 47?"],
    query_text="What is 128 multiplied by 47?",
    scope="global", scope_id=None,
)
if result is None:
    result = await run_llm(...)              # only on a miss
    await cache.store(
        ["qa", "What is 128 times 47?"],
        query_text="What is 128 times 47?",
        value=result, scope="global", scope_id=None,
    )

Use cache.lookup(...) / cache.store(...) for the combined exact-then-semantic flow. The exact cache is always tried first, so a semantic embedding is only computed on an exact miss (no extra cost when an exact hit is available).

Three ways to set the threshold

The similarity_threshold is a float in [0.0, 1.0]; a higher value demands a closer match. You can set it three ways, from lowest to highest precedence:

  1. Config field — the instance-wide default:

    CacheConfig(semantic_enabled=True, embedding_provider="local",
                similarity_threshold=0.92)
    
  2. Environment variable — read by MemoryCache.from_env():

    export MEMORY_REUSE_SEMANTIC_ENABLED=true
    export MEMORY_REUSE_EMBEDDING_PROVIDER=local
    export MEMORY_REUSE_SIMILARITY_THRESHOLD=0.90
    
    cache = MemoryCache.from_env()
    
  3. Per-call override — passed to a single lookup, taking precedence over the config/env value for that call only:

    await cache.lookup(key_parts, query_text="...", scope="global",
                       scope_id=None, threshold=0.98)
    

Returning just the relevant answer (extract_answer)

A semantic hit returns the whole stored answer by default. If you asked "Tell me about Python" and later ask "Who created Python?", the second query matches the first and returns the entire paragraph — even though only one sentence answers it.

Set extract_answer=True to have the cache return only the sentence(s) that best match the new question:

cache = MemoryCache(CacheConfig(
    semantic_enabled=True,
    embedding_provider="local",
    extract_answer=True,          # narrow the stored answer to the best sentence
    extract_min_similarity=0.5,   # confidence a sentence needs to be picked
))

Now "Who created Python?" returns just "Python is a high-level, interpreted programming language created by Guido van Rossum and first released in 1991." instead of the full paragraph.

How it works and its limits:

  • Purely extractive, no LLM. It splits the stored answer into sentences, embeds each with the same model, and returns the sentence closest to the query. It never calls an LLM and never fabricates — it can only return text already present in the stored answer.
  • Falls back to the full answer when no sentence clears extract_min_similarity, so you never get an empty result.
  • String answers only. Non-string values (dicts, numbers) and single-sentence answers are returned unchanged.
  • Best-effort, not QA. It returns a whole real sentence, so it can't reshape text into a crisp answer the way a model would. It is off by default.

Choosing where to use semantic matching

Semantic matching compares meaning, so it shines for natural-language questions where the same intent is phrased many ways — chatbots, FAQ agents, docs Q&A, search. That is exactly where it saves the most.

It's a similarity match, though, so keep it to reads and questions rather than correctness-critical commands. Two prompts can look close yet mean opposite things — "cancel order 123" vs "confirm order 123" — so use the exact cache for anything whose result depends on precise wording, especially actions with side effects. memory-reuse gives you two simple levers to stay on the safe side:

  • Tune the threshold. The default (0.95) favours precision — matches only fire when queries are very close. Raise it if you ever see a wrong match; lower it to trade some precision for a higher hit rate.

  • Opt a call out with exact_only=True. For a sensitive call site, skip the semantic cache entirely regardless of the global config:

    await cache.lookup(key_parts, query_text="...", scope="global",
                       scope_id=None, exact_only=True)   # exact match only
    

Used this way — similarity for questions, exact for commands — semantic caching is both safe and a big hit-rate win.

Latency and cost tradeoff

Enabling semantic caching adds an embedding computation on every exact miss. That embedding costs time (local model inference or an API round-trip) and, for hosted providers, money. The win is fewer full LLM calls when reworded queries match; the cost is the embedding overhead on misses. Enable it when your workload has many differently-worded but equivalent requests, so the saved LLM calls outweigh the embedding cost. An exact hit short-circuits before any embedding, so identical repeats stay as cheap as Phase 1.


Configuration reference

All options live on CacheConfig. Every field can also be set from an environment variable (read by MemoryCache.from_env()) where noted.

Field Type / accepted values Default Env var Description
backend "memory" | "redis" "memory" MEMORY_REUSE_BACKEND Storage backend. redis needs the [redis] extra.
redis_url str | None None MEMORY_REUSE_REDIS_URL Redis connection URL. Required when backend="redis".
default_ttl int > 0 | None 3600 MEMORY_REUSE_DEFAULT_TTL (int or "none") Default entry TTL in seconds. None never expires.
default_scope "global" | "user" | "session" "global" MEMORY_REUSE_DEFAULT_SCOPE Scope used when none is passed explicitly.
key_prefix str "memreuse" MEMORY_REUSE_KEY_PREFIX Prefix prepended to every cache key.
max_key_size int > 0 512 Max cache-key length in bytes.
enable_stats bool True MEMORY_REUSE_ENABLE_STATS (true/false) Track hit/miss/error counters.
semantic_enabled bool False MEMORY_REUSE_SEMANTIC_ENABLED (true/false) Turn on the semantic cache. Requires embedding_provider.
similarity_threshold float in [0.0, 1.0] 0.95 MEMORY_REUSE_SIMILARITY_THRESHOLD Minimum similarity to count as a match. Higher = stricter.
embedding_provider "openai" | "local" | "litellm" | None None MEMORY_REUSE_EMBEDDING_PROVIDER Which embedding backend to use. Required when semantic_enabled=True.
embedding_model str | None None (provider default) MEMORY_REUSE_EMBEDDING_MODEL Model name passed to the provider.
max_vectors_per_namespace int > 0 10000 Per-scope vector cap before LRU eviction.
store_exact_on_semantic_hit bool True On a semantic hit, also write an exact entry so the next identical request takes the faster exact path.
extract_answer bool False Return only the best-matching sentence(s) of a string answer on a semantic hit (extractive, no LLM).
extract_min_similarity float in [0.0, 1.0] 0.5 Confidence a sentence needs before extract_answer returns it instead of the full answer.

Invalid values raise at construction time: an out-of-range similarity_threshold or extract_min_similarity raises ConfigurationError; a non-positive default_ttl raises InvalidTTLError; enabling semantic_enabled without an embedding_provider raises ConfigurationError.


Cache statistics

stats = cache.stats
print(f"Hit rate: {stats.hit_rate:.1%}")
print(f"Hits: {stats.hits}  Misses: {stats.misses}")
print(f"Exact hits: {stats.exact_hits}  Semantic hits: {stats.semantic_hits}")
print(stats.to_dict())

hits always equals exact_hits + semantic_hits, so you can see how many of your hits came from the faster exact path versus semantic matching.


Examples

Runnable examples live in examples/:

  • basic_exact_cache.py — the cache primitives with no framework.
  • langgraph_agent_example.py — cached nodes and tools in a LangGraph-style flow.
  • langgraph_math_agent.py — a real ReAct agent with a calculator and a web-search tool, calling an LLM via LiteLLM.
  • semantic_cache_demo.py — a reworded query hitting the semantic cache via the combined lookup/store flow (offline, no model download).
  • semantic_agent.py — a real ReAct agent (calculator + web search) whose LLM calls run through the semantic cache with a local embedding model, so reworded questions reuse cached answers.
export API_KEY="your-groq-key"          # example uses Groq via LiteLLM
python examples/langgraph_math_agent.py

Roadmap

Phase Feature Status
1 Exact cache (LLM + tool), Redis backend, LangGraph + LiteLLM ✅ Shipped in v0.1
2 Semantic cache (embedding similarity, threshold control, answer extraction) ✅ Shipped in v0.2
3 Graph-level and node-level execution reuse Planned
4 Analytics dashboard, more framework integrations Planned

Documentation

Full documentation — guides plus an auto-generated API reference — is published at pranit-p.github.io/memory-reuse.

Build and preview it locally with the docs extra:

pip install -e ".[docs]"
mkdocs serve            # live preview at http://127.0.0.1:8000
mkdocs build --strict   # produce the static site in ./site

Contributing

Contributions are welcome. See CONTRIBUTING.md for setup, tests, and code-style guidelines, and CONTRIBUTORS.md for the list of people who have helped build this project.


License

MIT © Pranit Pawar

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

memory_reuse-0.2.0.tar.gz (92.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

memory_reuse-0.2.0-py3-none-any.whl (68.9 kB view details)

Uploaded Python 3

File details

Details for the file memory_reuse-0.2.0.tar.gz.

File metadata

  • Download URL: memory_reuse-0.2.0.tar.gz
  • Upload date:
  • Size: 92.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for memory_reuse-0.2.0.tar.gz
Algorithm Hash digest
SHA256 ec1698e8df04263eac1fac7c62c53008f469d4a69570b96814cd74677abd4b56
MD5 975a117b8245ddd9bed6a2d04759351f
BLAKE2b-256 1826a93ffa6f3ec662d31d63a12c97553522c3e785149959591c3df4642b288c

See more details on using hashes here.

Provenance

The following attestation bundles were made for memory_reuse-0.2.0.tar.gz:

Publisher: publish.yml on pranit-p/memory-reuse

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file memory_reuse-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: memory_reuse-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 68.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for memory_reuse-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 174d19833d20810fbbe0289ee8c2d1cab299572d9344d0b1b574bfd79af511d6
MD5 ed7d7f15c4e6041c3a80d05ccd043af4
BLAKE2b-256 e61dc2d5f50570a8c6c0c89bb6812f061226e91e98c836a2f9bbcebeb673dfc1

See more details on using hashes here.

Provenance

The following attestation bundles were made for memory_reuse-0.2.0-py3-none-any.whl:

Publisher: publish.yml on pranit-p/memory-reuse

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page