Skip to content

Commit f626636

Browse files
committed
docs(examples): add semantic-cache example (cache LLM responses by meaning)
A tiny semantic cache built on Moss: embed each question, look up the nearest one already answered, and return the stored answer if it clears a similarity threshold — skipping the model call entirely. Two phrasings of the same question ('what are your hours?' / 'when do you open?') hit the same cache entry, where an exact-text cache would miss and pay the model twice. Includes semantic_cache.py (the SemanticCache class + a runnable demo), README, and config. Retrieval is on-device and sub-10ms, far cheaper than the call it avoids.
1 parent bb03bca commit f626636

5 files changed

Lines changed: 160 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Moss
2+
MOSS_PROJECT_ID=Your moss project id here
3+
MOSS_PROJECT_KEY=Your moss project key here
4+
5+
# OpenAI — the model whose responses we cache
6+
OPENAI_API_KEY=Your openai api key here
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Semantic cache for LLM responses
2+
3+
Cache LLM answers by **meaning**, not by exact text.
4+
5+
A normal cache keys on the literal request string, so two phrasings of the same
6+
question miss:
7+
8+
```
9+
"what are your hours?" -> MISS -> calls the model
10+
"when do you open?" -> MISS -> calls the model again (paid twice)
11+
```
12+
13+
A semantic cache embeds the question and looks up the nearest one it has already
14+
answered. If it's close enough, it returns the stored answer with no model call.
15+
Repeat questions come back in single-digit-millisecond retrieval, on-device.
16+
17+
```
18+
"what are your hours?" -> MISS -> calls the model, stores the answer
19+
"when do you open?" -> HIT -> returns the stored answer, no model call
20+
```
21+
22+
## How it works
23+
24+
The whole thing is `SemanticCache` in [`semantic_cache.py`](./semantic_cache.py):
25+
26+
```python
27+
async def ask(self, question):
28+
hit = await self.index.query(question, QueryOptions(top_k=1))
29+
if hit.docs and hit.docs[0].score >= THRESHOLD: # close enough in meaning?
30+
return hit.docs[0].metadata["answer"] # cache hit — no LLM call
31+
answer = await call_the_model(question) # miss — ask once
32+
await self.index.add_docs(
33+
[DocumentInfo(id=question, text=question, metadata={"answer": answer})])
34+
return answer
35+
```
36+
37+
Moss keys the cache on the question's embedding and serves the nearest match
38+
in <10 ms locally, so the lookup is far cheaper than the model call it avoids.
39+
The one knob that matters is `THRESHOLD` (cosine similarity): too low and you
40+
answer questions people didn't quite ask; too high and you miss obvious matches.
41+
42+
## What you need
43+
44+
- A [Moss](https://moss.dev) account (`MOSS_PROJECT_ID` / `MOSS_PROJECT_KEY`)
45+
- An OpenAI key (the example uses `gpt-4o-mini` as the model being cached)
46+
- Python 3.10+
47+
48+
## Run
49+
50+
```bash
51+
uv sync # or: pip install moss openai python-dotenv
52+
cp .env.example .env # fill in your keys
53+
python semantic_cache.py
54+
```
55+
56+
Expected: the first question is a `MISS` (calls the model), the paraphrased
57+
second question is a `HIT` (returns instantly, no model call).
58+
59+
## Resources
60+
61+
- [Docs](https://docs.moss.dev)
62+
- [GitHub](https://github.com/usemoss/moss)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[project]
2+
name = "moss-semantic-cache"
3+
version = "0.1.0"
4+
description = "Semantic cache for LLM responses, built on Moss"
5+
requires-python = ">=3.10"
6+
dependencies = [
7+
"moss",
8+
"openai>=1.0",
9+
"python-dotenv>=1.0",
10+
]
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Semantic cache for LLM responses, built on Moss.
2+
3+
A normal cache keys on the exact text of a request, so two ways of asking the
4+
same thing ("what are your hours?" / "when do you open?") miss and you pay the
5+
model twice. A *semantic* cache keys on meaning: embed the question, look up the
6+
nearest one you've already answered, and if it's close enough, return the stored
7+
answer without calling the model.
8+
9+
The whole thing is the SemanticCache class below. The one knob that matters is
10+
the similarity threshold.
11+
"""
12+
13+
import asyncio
14+
import os
15+
import time
16+
17+
from dotenv import load_dotenv
18+
from openai import AsyncOpenAI
19+
20+
from moss import MossClient, DocumentInfo, QueryOptions
21+
22+
load_dotenv()
23+
24+
# cosine similarity above which a cached answer is "close enough" to reuse.
25+
# too low -> you answer questions people didn't quite ask; too high -> you miss.
26+
THRESHOLD = 0.92
27+
28+
moss = MossClient(
29+
project_id=os.getenv("MOSS_PROJECT_ID"),
30+
project_key=os.getenv("MOSS_PROJECT_KEY"),
31+
)
32+
llm = AsyncOpenAI()
33+
34+
35+
class SemanticCache:
36+
"""A tiny vector index of past questions -> answers, queried by meaning."""
37+
38+
def __init__(self, index):
39+
self.index = index
40+
41+
async def ask(self, question: str) -> tuple[str, bool]:
42+
# 1. look for the closest question we've already answered
43+
hit = await self.index.query(question, QueryOptions(top_k=1))
44+
if hit.docs and hit.docs[0].score >= THRESHOLD:
45+
return hit.docs[0].metadata["answer"], True # cache hit — no LLM call
46+
47+
# 2. miss: ask the model once
48+
resp = await llm.chat.completions.create(
49+
model="gpt-4o-mini",
50+
messages=[{"role": "user", "content": question}],
51+
)
52+
answer = resp.choices[0].message.content
53+
54+
# 3. remember it so any wording of it is instant next time
55+
await self.index.add_docs(
56+
[DocumentInfo(id=question, text=question, metadata={"answer": answer})]
57+
)
58+
return answer, False
59+
60+
61+
async def main():
62+
# a fresh in-memory session index acts as the cache for this run
63+
index = await moss.session("qa-cache")
64+
cache = SemanticCache(index)
65+
66+
# the 2nd question means the same as the 1st, phrased differently -> cache hit
67+
questions = [
68+
"What are your opening hours?",
69+
"when do you open?",
70+
"How do I reset my password?",
71+
]
72+
for q in questions:
73+
t = time.perf_counter()
74+
answer, hit = await cache.ask(q)
75+
ms = (time.perf_counter() - t) * 1000
76+
tag = "HIT " if hit else "MISS"
77+
print(f"[{tag} {ms:7.1f} ms] {q}\n -> {answer.strip()[:90]}\n")
78+
79+
80+
if __name__ == "__main__":
81+
asyncio.run(main())

0 commit comments

Comments
 (0)