A small, readable memory layer for LLM apps: a simplified mem0 clone.
It extracts durable facts from conversations, stores them as embeddings, and (crucially) reconciles new facts against what it already knows instead of blindly appending. So your app can "remember" a user across sessions: that they're vegetarian, that they moved from Pune to Bangalore, that they no longer eat meat.
conversation ──▶ extract facts ──▶ embed ──▶ search similar ──▶ decide ──▶ ADD / UPDATE / DELETE / NOOP
A naive memory is "embed every message and append." That accumulates duplicates and keeps stale, contradicted facts forever. The interesting part of mem0, and of this clone, is the update decision: before storing a new fact, look at similar existing memories and choose one of four operations.
| Event | When | Example |
|---|---|---|
ADD |
Genuinely new information | new: "Allergic to peanuts" |
UPDATE |
Refines / corrects an existing memory (same id) | "Lives in Pune" → "Lives in Bangalore" |
DELETE |
Contradicts an existing memory, no replacement | "Eats meat" removed after "I stopped eating meat" |
NOOP |
Already known | "Lives in Pune" stated again |
This is the most important logic in the codebase. See memory/memory.py
(add → _gather_candidates → _decide → _apply) and the prompts in
memory/prompts.py.
- Extract atomic facts from the conversation with one LLM call
(
{"facts": ["Lives in Pune", "Is vegetarian"]}). - Embed each fact and search the user's existing memories for similar ones.
- Reconcile in a single LLM call. The retrieved memories are presented to the LLM as a
numbered list (integer id to real UUID map), so the LLM only ever sees small integers and
cannot hallucinate a database id. It returns one operation per fact:
{"memory": [ {"id": 0, "text": "Lives in Bangalore", "event": "UPDATE", "old_memory": "Lives in Pune"}, {"id": 7, "text": "Is vegetarian", "event": "ADD"} ]} - Apply each operation: write to the vector store + SQLite, and append a row to the
history log. If no similar memories exist, the LLM call is skipped and every fact is an
ADD. Malformed ids (e.g. anUPDATEto a non-existent memory) fail safe toADD.
Embeds the query and returns the most similar memories for that user, with a similarity score. SQLite is the source of truth for the memory text; the vector store only holds embeddings + ids.
Basic management. History is recorded before a delete, so the change log of a memory
(ADD → UPDATE → DELETE) survives even after the memory itself is gone.
memory/ core library (no FastAPI dependency)
├── memory.py the Memory class: add / search / get_all / delete / history
├── prompts.py fact-extraction + update-decision prompts
├── llm.py BaseLLM + OpenAI / Ollama backends + get_llm() factory
├── embeddings.py SentenceTransformerEmbedder (prod) + HashEmbedder (offline/tests)
└── store.py SQLiteMetadataStore + ChromaVectorStore + InMemoryVectorStore
api/
└── main.py FastAPI app exposing the routes
tests/
└── test_memory.py pytest tests for ADD / UPDATE / DELETE / NOOP, search, history
demo.py multi-turn conversation demo (offline by default)
Everything is built from four pluggable parts (an LLM, an embedder, a vector store, and a
metadata store) injected into Memory. Production uses Chroma + sentence-transformers + SQLite +
your chosen LLM; the tests swap in fakes so the decision logic is exercised with no network and no
model download.
All LLM access goes through memory/llm.py. Select a backend with env vars:
# Local model via Ollama (default)
export MEM0_LLM_PROVIDER=ollama
export MEM0_LLM_MODEL=llama3.1
# Hosted / OpenAI-compatible API
export MEM0_LLM_PROVIDER=openai
export MEM0_LLM_MODEL=gpt-4o-mini
export OPENAI_API_KEY=sk-...
# export OPENAI_BASE_URL=... # any OpenAI-compatible endpointpython -m venv .venv && source .venv/bin/activate
pip install -r requirements.txtThe demo runs fully offline by default (a rule-based stand-in LLM + a lexical hash embedder, no downloads) so you can see the loop immediately:
python demo.pyIt feeds a 4-turn conversation, prints the ADD/UPDATE/DELETE/NOOP decision per turn, shows that "Lives in Pune" is replaced by "Lives in Bangalore", and recalls the vegetarian fact stated in turn 1. To use the real stack (sentence-transformers + Chroma + your LLM):
python demo.py --realuvicorn api.main:app --reload| Method | Path | Body / Query |
|---|---|---|
| POST | /memories |
{"user_id": "...", "messages": [{"role": "user", "content": "..."}]} |
| GET | /memories/search |
?user_id=&query=&top_k=5 |
| GET | /memories |
?user_id= |
| DELETE | /memories/{id} |
(none) |
| GET | /memories/{id}/history |
(none) |
# add
curl -X POST localhost:8000/memories -H 'content-type: application/json' \
-d '{"user_id":"ravi","messages":[{"role":"user","content":"I live in Pune and I am vegetarian"}]}'
# search
curl 'localhost:8000/memories/search?user_id=ravi&query=where%20do%20they%20live'
# history of one memory
curl localhost:8000/memories/<id>/historypytestThe tests drive the memory loop with a scripted fake LLM and the offline hash-embedder / in-memory vector store, so the ADD / UPDATE / DELETE / NOOP decision, per-user search scoping, and the history log are all verified deterministically, with no API keys and no model downloads.
- Single batched decision call over all extracted facts + retrieved candidates (like mem0), rather than one call per fact. This lets the LLM consolidate related facts and resolve contradictions across the whole batch.
- Integer-id indirection for retrieved memories prevents the LLM from inventing UUIDs.
- Kept simple on purpose: no entity/graph store, no async batching, no lemmatized hash dedup,
no agent/run scoping. Just the
user_id-scoped fact memory that makes the add/update loop clear.