Cortex Layer
SDK

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 extraction
from 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 pass auto_relink=True to relink after every add, 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) Memory falls 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 with entities(text) and sentences(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); pass chat=fn(prompt, model) -> str to use any model.
Memory methodReturns
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: llm can be a dict, a callable fn(system, user) -> str, or any object with generate(); embedder any object with embed_batch(texts, action) and a name. 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). The raw engine costs none.
  • Relative dates: like Mem0, the extractor resolves "yesterday" against today unless told the conversation's date. observation_date_from_timestamp=True passes your add(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=False gives 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 / delete work 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+.

On this page