Embedded — Memory
Run the Cortex engine in your own process, with no server and no API key.
Memory is the Cortex engine, embedded directly in your process. Nothing leaves your machine.
pip install "cortexlayer[local]"
python -m spacy download en_core_web_sm # optional, better entity extractionfrom cortexlayer import Memory
m = Memory() # ~/.cortexlayer, or Memory("path/to/dir")
m.add("Christopher Nolan directed Inception.", user_id="alice")
m.add("Christopher Nolan was born in London in 1970.", user_id="alice")
m.relink(user_id="alice") # batch linking pass: run after adding several
m.search("Who directed Inception?", user_id="alice", limit=1)
# [SearchResult(title='Christopher Nolan directed Inception.', via='direct', …),
# SearchResult(title='Christopher Nolan was born in London in 1970.',
# via='link', linked_from='…')] <- pulled in through the shared entity- One store, many users: every call takes an optional
user_id; each user gets an isolated collection, so users can never see each other's pages. Omit it and the default user is used. - Linking is a batch pass, never per insert: call
relink()after adding memories (or passauto_relink=Trueto relink after everyadd, which costs a scan of all pages). - Local and private: memories live in an embedded Chroma store under
data_dir. Nothing leaves your machine (Chroma's anonymous telemetry is switched off). Two one-time downloads: Chroma's small ONNX embedding model on first use (~80 MB), and the spaCy model if you install it. - No model? It still runs. With
entity_extractor="auto"(the default)Memoryfalls back to a simpler regex extractor, with a one-time warning, if spaCy or its model is missing. Entities drive linking, so the fallback finds fewer links. Force a choice with"spacy"or"regex", or pass your own object withentities(text)andsentences(text)methods. - Short answers (optional):
m.answer("Where did Alice move?")retrieves and has an LLM distil a direct answer plus the supporting page ids. It uses a local Ollama by default ($OLLAMA_HOST); passchat=fn(prompt, model) -> strto use any model.
Memory method | Returns |
|---|---|
add(text, user_id=, timestamp=) | AddResult (long text is chunked into several pages) |
search(query, user_id=, limit=4, expand_links=True) | list[SearchResult] |
get(id) / get_all(query=, limit=, offset=) | Page / PageList |
update(id, text) / delete(id) / delete_all(user_id=) | None / None / count removed |
relink() / count() | {"pages", "links_written"} / int |
answer(query, limit=, model=, chat=) | Answer(answer, source_page_ids) |
Memory.from_config({...}) builds one from a dict (data_dir, entity_extractor, spacy_model,
default_user_id, auto_relink, backend, llm, embedder, custom_instructions,
observation_date_from_timestamp, keyword_scoring).
Fact memory: Memory(backend="facts")
The default raw engine stores your text as small pages and never calls an LLM. facts works
like Mem0: each add asks an LLM to distil the text into self-contained facts (resolving dates
and pronouns), stores those, and boosts search results by the entities they share with your query.
m = Memory(
backend="facts",
llm={"model": "qwen3.5:9b"}, # Ollama at $OLLAMA_HOST by default
embedder={"provider": "ollama", "model": "qwen3-embedding:8b"}, # default: Chroma's built-in ONNX model
)
m.add(
"[8 May, 2023] Caroline: I moved to Lisbon last week and adopted a dog named Max.",
user_id="alice",
)
m.search("What is Caroline's dog called?", user_id="alice") # -> "Caroline adopted a dog named Max ..."- Same API as the raw engine, including
relink()and link-expansion (links are entity overlap over the extracted facts). - Bring your own model:
llmcan be a dict, a callablefn(system, user) -> str, or any object withgenerate();embedderany object withembed_batch(texts, action)and aname. A store records which embedder made its vectors and refuses to open with a different one. - Failure is loud: an unreachable LLM or embedder raises
LLMError(nothing is stored). "The model found nothing worth remembering" is a normal empty result. - It costs an LLM call per
add(plus embeddings). Therawengine costs none. - Relative dates: like Mem0, the extractor resolves "yesterday" against today unless told
the conversation's date.
observation_date_from_timestamp=Truepasses youradd(timestamp=...)as that date. - Exact words: embedding scores are often compressed into a narrow band, so a fact that
literally contains a query word can rank below generic ones. Search fuses a BM25 keyword score by
default (
keyword_scoring=Falsegives plain semantic + entity scoring, identical to Mem0 on Chroma). - Facts are ordinary pages: each fact is stored as a page in the user's Chroma collection, so
links are persisted and
get/get_all/update/deletework on facts exactly as on raw pages.
The extraction prompt and pipeline are adapted from Mem0
(Apache-2.0); cortexlayer does not depend on the mem0ai package.
Python 3.10+.