Python binding migration guide: 0.2.x → 0.3.0¶
membrick 0.3.0 cleans up the Python binding around the one supported surface: MemoryService. Most existing Memory calls keep working, but there are two intentional breaks. This guide covers what changed, what stayed compatible, and what the new knobs actually do.
Breaking changes¶
1. RemoteMemory is gone¶
# 0.2.x
remote = membrick.RemoteMemory("http://127.0.0.1:8080", api_key="token")
# 0.3.0 — same behavior, one class
remote = membrick.Memory(base_url="http://127.0.0.1:8080", api_key="token")
Pitfall: base_url must be passed by keyword. The first positional argument of Memory(...) is path, so membrick.Memory("http://127.0.0.1:8080") does not connect to a server — it silently creates a local index in a directory literally named http://127.0.0.1:8080. This fails silently, not loudly; always use the keyword form.
2. Python IndexStore binding removed¶
The membrick.IndexStore class no longer exists. Python applications must go through membrick.Memory, which is the only supported external API.
Rust integrations use MemoryService; stores and indexes are internal. If you were using IndexStore from Python, migrate to Memory or stay on the 0.2.x line.
What stayed compatible¶
Memory(path=...)/Memory()— same as before; new options are keyword-only additions at the end.search(query, user_id, top_k)— the original three positional arguments still work in the same order.top_know defaults to10, sosearch(query, user_id)is valid.add,get,list,update,delete,refresh— argument order unchanged, includingdelete(user_id, memory_id).membrick.version()returns the package version ("0.3.0").
New in 0.3.0¶
Constructor options (local backend only)¶
memory = membrick.Memory(
path="./memory-index",
ner_provider="heuristic", # 'heuristic' (default, Python-free) | 'spacy'
spacy_model=None, # explicit override; default follows detected language
)
Language is auto-detected by the engine. There is no language knob: the engine detects the content language per text (script statistics plus language signals) and selects per-language defaults — notably the spaCy model — automatically. An explicit spacy_model always wins. (An earlier draft of this binding exposed a language option; it was removed when the engine moved to auto-detection.)
Remote backends ignore these knobs. The options are validated client-side (bad values still raise ValueError), but the server owns its configuration — there is no way to configure a remote server from the Python client.
Session / scope / filters on search¶
memory.search(
"what did we decide about the API?",
"user-1",
top_k=10,
session_id="session-1", # enables follow-up resolution + rerank
scope="team-a", # conversation-state scope (not a doc filter)
filters={"request_id": "req-42"}, # document filters, see below
)
session_idis a conversation-state key, not a document-session filter: it loads bounded prior state so follow-up phrasing ("what about the Python one?") resolves against the conversation. It never becomes an implicit ownership filter —user_idstill scopes document visibility.scopelets the conversation state be shared under a different key than the document-owninguser_id. Ownership is still enforced onuser_id.filtersare forwarded to the engine's document filter path. Their exact contract is pinned by the test suite (tests/python/test_memory.py,TestFilterContract):- Filters are ANDed, but a filter whose key has no index postings — or whose key exists but value has no postings — is silently dropped, not exclusionary.
{"request_id": "no-such"}returns all owned docs. - Ownership (
memory_user_id) is always enforced and cannot be overridden by caller filters (a caller-suppliedmemory_user_idis stripped; a literal"user_id"key is unknown and dropped). - Structured-fact bypass: the structured-fact retrieval arm is built with
filters=None— it gates only on ownership, supersession, and expiry. A fact question (e.g. "Which city have both Gina and Jon visited?") can therefore return documents the caller's filters would have excluded on the lexical path, ranked at the top (score > 1000). This is current engine behavior, not a binding bug; changing it is a retrieval change that needs an explicit decision. - In short: filters are best-effort narrowing, not a security boundary. Ownership and expiry are always enforced regardless of filters.
add_batch¶
responses = memory.add_batch([
{"request_id": "r1", "user_id": "u", "session_id": "s",
"messages": [{"role": "user", "content": "..."}]},
# ... up to 128 entries
])
At most 128 entries per call (larger batches raise ValueError; split them client-side). Local batching applies entries sequentially with a single refresh at the end; there is no rollback — if entry N fails, entries before N are already stored. Retrying with the same request_ids is idempotent for the entries that succeeded. Against a remote server the client falls back to sequential /v1 adds (the server exposes no batch endpoint) and stops at the first error.
Thread safety¶
Memory releases the GIL during engine/server calls, but the instance is not thread-shareable (unsendable in pyo3 terms): do not use one Memory from multiple Python threads. Create one instance per thread, or serialize access with a lock.