AAI Agentic Handbook
Problem 114 · LLM Systems

Tenant-Scoped Semantic Cache with TTL

MediumAI engineering
01Problem

Build `SemanticCache(embed, threshold, ttl_seconds, max_per_tenant, clock)`. `get(tenant, query)` first tries an exact hit on the normalised query (NFKC, casefold, collapse whitespace, strip trailing `?!.`), then falls back to the most similar cached query **in the same tenant** with cosine >= `threshold`. Expired entries (older than TTL by the injected clock) never hit. `put(tenant, query, answer)` evicts the least recently used entry when a tenant exceeds `max_per_tenant`.

02Key insight

The tenant must be part of the key, not a filter applied afterwards: a cache that returns tenant A's answer to tenant B is a data breach, not a cache miss. Normalisation buys cheap exact hits for trivially different phrasings; the semantic fallback catches paraphrases but is where wrong answers come from, so the threshold is a precision knob you set from labelled pairs, not a guess. TTL bounds staleness when the underlying documents change, and an injected clock makes all of that testable without sleeping.

03Solution

Try it first.

Timeget O(m * d) for m live entries in the tenant (exact hit O(1)); put O(d) amortised
SpaceO(tenants * max_per_tenant * d)
tests.py · 9 passing
tests.pypython22 lines
VOCAB = ["refund", "policy", "return", "shipping", "time", "price"]
SYN = {"returns": "return", "refunds": "refund"}
def embed(text):
    words = [SYN.get(w, w) for w in re.findall(r"[a-z]+", text)]
    return [float(words.count(v)) for v in VOCAB]
now = [0.0]
c = SemanticCache(embed, threshold=0.9, ttl_seconds=60, max_per_tenant=2, clock=lambda: now[0])
c.put("acme", "What is the refund policy?", "30 days")
assert c.get("acme", "  what IS the   refund policy ") == "30 days"
assert c.get("globex", "What is the refund policy?") is None
assert c.get("acme", "refunds policy") == "30 days"
assert c.get("acme", "shipping time") is None
now[0] = 61.0
assert c.get("acme", "What is the refund policy?") is None
c.put("acme", "refund policy", "A")
c.put("acme", "shipping time", "B")
now[0] = 62.0
assert c.get("acme", "refund policy") == "A"
c.put("acme", "price", "C")
assert c.get("acme", "shipping time") is None
assert c.get("acme", "refund policy") == "A" and c.get("acme", "price") == "C"
assert normalise("Hello  World!?") == "hello world"
04Staff-level follow-up

At scale the linear scan becomes a per-tenant ANN index (or a vector DB namespace per tenant). Then cover invalidation: key entries by the corpus or prompt version so a document update or prompt change cannot serve stale answers, never cache answers that used per-user permissions unless the ACL set is in the key, and track the false-hit rate by sampling cache hits for judging.