Skip to main content

Atomic memory infrastructure for agents — atomic facts on write, atomic sub-question decomposition on read.

Project description

atomir

Atomic memory infrastructure for agents. Memory is atomic on both ends: atomic facts on write (extract → reconcile), atomic sub-question decomposition on read (decompose → retrieve per sub-question → union).

The thesis

Most memory systems store raw text blobs and retrieve with a single fuzzy similarity search. atomir does the opposite at both ends:

  • Write — a message is split into small, self-contained facts, and each is reconciled into memory (ADD new, UPDATE a changed value keeping history, DELETE what's no longer true, NOOP duplicates). A similarity gate biases toward ADD so distinct facts never over-merge.
  • Read — a question is decomposed into atomic sub-questions (only when it helps), each retrieved independently, then results are unioned. This surfaces facts a single whole-question embedding misses.

Vendor-neutral by construction

The LLM, the embedder, and the vector store are each an interface chosen at runtime by config ({provider, config} blocks). The engine imports only the interfaces — never a provider SDK or vendor name. Swapping providers is one config change; adding a new one is a single class plus a registry line. Defaults use fake backends, so everything runs with no external keys.

Slot Built-in providers
LLM fake, groq, openai, anthropic, ollama
Embedder fake, jina, voyage, openai, ollama
Store json, qdrant

Each provider is selected by LLM_BACKEND / EMBED_BACKEND / STORE_BACKEND with its key in LLM_API_KEY / EMBED_API_KEY (Ollama and the fakes need none). LLM_BASE_URL / EMBED_BASE_URL can point at self-hosted or proxy endpoints.

Install

pip install -e .                 # core (offline: fake LLM + fake embedder + JSON store)
pip install -e ".[qdrant]"       # add the Qdrant backend
pip install -e ".[api]"          # add the FastAPI server
pip install -e ".[all]"          # everything

groq and jina need no extra — they call their HTTP APIs over the standard library.

Quickstart — embedded, no Docker

Runs fully offline with the default fake backends:

from atomir.assembly import build_memory_service

mem = build_memory_service()                      # backends chosen by .env
mem.add("user123", "I'm vegetarian and my manager is Dana Lopez.")
mem.add("user123", "I'm working on Project Atlas.")

hits = mem.search("user123", "who should I email about my project?")
print(hits["subquestions"])                       # the sub-questions it asked
for r in hits["results"]:
    print(r["text"], round(r["score"], 3))

mem.get_all("user123")
mem.delete("user123", fact_id)
mem.reset("user123")

To use real providers, copy .env.example to .env and set the keys/backends.

Production — Docker Compose (API + Qdrant server)

cp .env.example .env             # optional: add real keys; without it, LLM/embedder run fake
docker compose up --build        # brings up the API and a Qdrant server

The API points at the Qdrant service via STORE_URL=http://qdrant:6333. Then:

curl -XPOST localhost:8000/memories -H 'content-type: application/json' \
  -d '{"user_id":"u1","text":"My manager is Dana."}'
curl -XPOST localhost:8000/search -H 'content-type: application/json' \
  -d '{"user_id":"u1","query":"who is my manager?"}'

HTTP endpoints

Method Path Body / query Returns
POST /memories {user_id, text} {operations, facts}
POST /search {user_id, query, k?, decompose?} {subquestions, results}
POST /answer {user_id, query, k?, decompose?} {answer, subquestions, results}
GET /memories ?user_id= list of facts
DELETE /memories/{id} ?user_id= {deleted, id} (404 if absent)
DELETE /memories ?user_id= {reset}
GET /health {status, store, llm, embedder}

MemoryClient(base_url) (in atomir.client) wraps these with the same method names and return shapes.

Configuration

All config is read from the environment (see .env.example): LLM_BACKEND, LLM_API_KEY, MODEL, EMBED_BACKEND, EMBED_API_KEY, EMBED_DIM, RECONCILE_MIN_SIM, STORE_BACKEND, COLLECTION, STORE_URL, STORE_PATH.

Composed answers

search returns facts (+ sub-questions); answer additionally composes a grounded final sentence from them (LLM told to use only the retrieved facts):

mem.answer("user123", "who should I email about my project?")
# -> {"answer": "...", "subquestions": [...], "results": [...]}

Robustness

Provider calls retry transient failures (HTTP 429 rate-limits and 5xx) with backoff, honoring Retry-After. The JSON store writes atomically (temp file → fsyncos.replace), so a crash mid-write can't corrupt the file.

Known limitations

  • RECONCILE_MIN_SIM is embedder-dependent. The default 0.5 is tuned for Jina via eval/tune.py (it sits between measured unrelated ~0.45 and same-attribute ~0.60 similarity). Switching embedders? Re-run eval/tune.py.
  • JSON store is single-process. Writes are now atomic (no corruption), but it holds no cross-process lock and rewrites the whole file per save — great for dev and small deployments; use Qdrant at scale.
  • No multi-fact transactions. Each write is atomic and per-user serialized (Step 9), and a partial add is self-healing on retry (reconcile NOOPs facts already stored). Full all-or-nothing rollback across an add is deferred (DECISION #5) — open an issue if you need it.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

atomir-0.3.0.tar.gz (29.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

atomir-0.3.0-py3-none-any.whl (40.9 kB view details)

Uploaded Python 3

File details

Details for the file atomir-0.3.0.tar.gz.

File metadata

  • Download URL: atomir-0.3.0.tar.gz
  • Upload date:
  • Size: 29.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for atomir-0.3.0.tar.gz
Algorithm Hash digest
SHA256 6579cad315304e4b978c742337db342fd6e9e95a189b763e96fa459a3c38ce13
MD5 f1108b93ec9dd56c36a4e760fca45dc8
BLAKE2b-256 c9811719ce2a481f3ef0083fa00a4d8cdab1dcf9b96777ba7b80e4ad57603694

See more details on using hashes here.

File details

Details for the file atomir-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: atomir-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 40.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for atomir-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4f75a235b79a1383735fb0b93c9969c835564167970f62dc248d85ff4b096a76
MD5 fd831047be1de7fdfe4d335bd17496ea
BLAKE2b-256 4cc7da4dba26efd900d9ed648c91c9871b032b701bf26de13c2efc212cfa8037

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page