Getting started with Python
Python talks to SodaMem as a library. There is no server in the loop and no network hop — import sodamem and you are already past it.
Install
pip install "sodamem[chroma,llm]"Python 3.11 or newer. The two extras are doing different jobs:
chroma— vector search plus the local ONNX embedder.SodaMem.open()needs this.llm— OpenAI-compatible providers (OpenAI / DeepSeek / Gemini wire format). Writing needs one; reading never does.
See Install extras for the full list.
Credentials
Extraction turns a conversation into facts, and that takes a model. Set the same four variables the server uses, so an embedded process and a container are configured identically:
export SODAMEM_LLM_PROVIDER=openai # openai | anthropic | deepseek | gemini
export SODAMEM_LLM_API_KEY=sk-...
export SODAMEM_LLM_MODEL= # provider default if empty
# export SODAMEM_LLM_BASE_URL= # only for a non-api.openai.com endpointReading needs none of this
search and build_context are the zero-LLM tier. Drop the extractor argument and a read-only store works with every variable above unset.
Your first store
from sodamem import SodaMem
from sodamem.llm import create_provider_from_env
from sodamem.memory.ingest.extractor import FactEventExtractorV2
# Writing needs a model to extract facts with; reading never does.
mem = SodaMem.open("./data", extractor=FactEventExtractorV2(create_provider_from_env()))
mem.ingest(
[{"role": "user", "content": "Actually I moved from Kauai to Oahu."}],
user_id="u1", session_id="s1", session_time="2023-05-25",
)
block = mem.build_context("where am I staying?", user_id="u1", token_budget=1000)
print(block.text) # prompt-ready — zero LLM calls
print(block.citations) # the exact evidence behind every line of itSodaMem.open() creates ./data if it isn't there.
What comes back
build_context does not hand you a list of records to assemble yourself. It returns a block that is ready to drop into a prompt, already deduplicated and already inside the token budget you asked for, plus the citations that produced it:
evidence_id = ev_fact:fact_6ada707b…
support = "Can you recommend a good beach on Oahu that's not too crowded?"
predicate = user wants a not-too-crowded beach on Oahu
entities = location=Oahu | occasion=birthday
source = session_40 / turn_10 ← the exact turn, not "some chat"
date = 2023-05-25Each citation is a link into the evidence chain, not a relevance score.
Corrections
Tell it something that contradicts what it knows and nothing is overwritten:
mem.ingest(
[{"role": "user", "content": "I'm back on Kauai now."}],
user_id="u1", session_id="s2", session_time="2024-02-01",
)The earlier fact closes with a valid_until and a SUPERSEDES edge connects the two. It leaves search and context, and it stays readable. That is the whole difference from a delete — see Four time axes.
When to reach past build_context
| you need | use | LLM calls |
|---|---|---|
| context for a prompt | build_context | none |
| raw hits to post-process | search | none |
| "list every X you know about me" | build_context(organizer=...) | an organizer pass |
| a hard multi-hop question answered | answer | a planner loop |
The organizer argument is Python-only on purpose: /v1/context never accepts one, so the zero-LLM guarantee on that route cannot be flipped by a request parameter. Retrieval tiers explains the trade.
Nothing leaves the machine
No telemetry, no analytics, no callback. The only outbound request a default install ever makes is a one-time download of the 90 MB MiniLM embedding model into ~/.cache/chroma/. Pre-seed that cache and it runs air-gapped.
Next
- Coding assistants — memory for Claude Code, Cursor, Codex
- The evidence chain — what the citations actually point at
- HTTP API — the same operations over the wire

