Skip to main content

atomir

PyPI PyPI Downloads Python

Atomic memory for LLM agents — atomic on both ends: facts are extracted and reconciled on write; questions are decomposed into sub-questions on read.

Why

Most memory systems store text blobs and retrieve with one fuzzy search. atomir doesn't:

  • Write — split a message into atomic facts, then reconcile each (ADD / UPDATE-with-history / DELETE / NOOP). A similarity gate stops distinct facts over-merging.
  • Read — decompose a question into sub-questions (only when useful), retrieve each, union the results. Surfaces facts a single-blob search misses.

Remember when, not just what. atomir adds an episodic layer: every message becomes a time-ordered event on a per-relationship timeline, and the fact store becomes a projection of that event log. When a state changes — a new job, a move, a new manager — the old value isn't overwritten and lost; it stays on the timeline. Temporal questions ("what job did I have before this one?", "who was my manager in 2023?") are answered by walking the timeline, not by similarity search — deterministic, self-hosted, no graph database. Two atomic ends (facts on write, sub-questions on read) over a temporal spine: facts answer now, events answer when.

Vendor-neutral: LLM, embedder, and store are interfaces chosen by config. Defaults are fake, so it runs with no keys.

Install

pip install atomir                          # core, offline-capable
pip install "atomir[qdrant,api]"            # Qdrant backend + HTTP API
pip install "atomir[langchain,langgraph]"   # framework integrations

Quickstart

from atomir.assembly import build_memory_service

mem = build_memory_service()          # backends from .env; defaults to fake (no keys)
mem.add("user123", "I'm vegetarian and my manager is Dana.")

hits = mem.search("user123", "who should I email about my project?")
print(hits["subquestions"], [r["text"] for r in hits["results"]])

mem.answer("user123", "who is my manager?")   # composed answer + the facts used
mem.get_all("user123"); mem.delete("user123", fact_id); mem.reset("user123")

Real providers: copy .env.example.env, then set backends + keys.

Providers

Slot Options Config
LLM fake groq openai anthropic gemini azure_openai ollama LLM_BACKEND, LLM_API_KEY, MODEL
Embedder fake jina voyage openai gemini azure_openai ollama EMBED_BACKEND, EMBED_API_KEY, EMBED_DIM
Store json qdrant STORE_BACKEND, STORE_URL / STORE_PATH

Adding a provider is one class + one registry line. LLM_BASE_URL / EMBED_BASE_URL target self-hosted or proxy endpoints.

Retrieval: reads fuse dense (embedding) + lexical (BM25) rankings via RRF and run sub-question retrievals concurrently; set HYBRID_SEARCH=false for dense-only. Provider calls retry transient failures (rate limits, connection resets).

Episodic memory (optional, experimental)

Set EPISODIC_ENABLED=true for an event-log layer alongside the atomic facts. Messages become time-ordered events grouped into per-entity, per-verb branches; the fact store becomes a projection of that log — so a "left Beta, joined Acme" message leaves exactly one live employer (Acme) with Beta in history, and the timeline keeps the transition.

Arbitration — facts answer now, events answer when. Reads route each sub-question: current → facts, temporal → a deterministic chain walk, semantic → the usual hybrid search.

mem.add("u", "I left Beta in November and joined Acme Corp.")
mem.timeline("u", branch="works_at")   # ordered events (when things changed)
mem.forget("u", "Alex")                # cascade-delete everything about an entity

New surface (all additive): timeline(...), forget(entity), GET /timeline, POST /forget, MCP timeline / forget_about tools, and atomir migrate --backfill --user <id> for pre-episodic stores. Off by default, so upgrading changes nothing until you opt in.

General-purpose by default. No ontology is assumed: branches emerge from your own messages, kept consistent by feeding the existing registry back into the extractor/namer/planner. The namer produces knowledge-graph predicates (joinedworks_at) so a query planner's natural hint resolves. For a known domain, opt into a pack: ONTOLOGY_PACK=personal seeds ~18 predicates.

Branch matching strips entity names before embedding, uses a three-zone decision (auto-assign / LLM judge / new), and resolves read-time hints by exact match then relative-best. Every threshold is embedder-dependent — run python -m eval.episodic.branch_microeval --write to calibrate BRANCH_MATCH_AUTO / BRANCH_MATCH_GRAY_LOW / BRANCH_RESOLVE_FLOOR / BRANCH_RESOLVE_MARGIN for your embedder.

Temporal questions are answered by walking a timeline rather than by similarity search, so it recovers historical states that overwrite-based memory loses (a former employer, a previous city). Deterministic, self-hosted, no graph DB. An evaluation harness is included (eval/episodic/); formal benchmark results will be published separately.

Honest limits: multi-entity graph queries (out of scope — Graphiti-class systems own that); entity resolution is exact-alias by default (under-merges rather than risk a wrong merge; ENTITY_V2=true adds embedding+LLM resolution); JSON episodic store is dev-scale (append-heavy — pair with SQLite/Qdrant at scale); and branch-naming quality varies with model strength — weak models name inconsistently, mitigated by the KG-predicate namer and the calibration harness, but a stronger model or a pack gives the most reliable chain walks.

Agent frameworks

atomir is the memory, not the model: recall before, remember after. Scope memory by user_id"user:1" (shared), "user:1#agent:x" (agent-private), "acme|user:1" (multi-tenant).

LangChainAtomirRetriever is a real BaseRetriever:

from atomir.integrations.langchain import AtomirMemory
mem = AtomirMemory(build_memory_service(), user_id="user:1")
retriever = mem.as_retriever()

LangGraph — drop-in nodes for multi-agent graphs:

from atomir.integrations.langgraph import recall_node, remember_node
g.add_node("recall", recall_node(mem))       # -> state["memories"]
g.add_node("remember", remember_node(mem))   # stores state["input"]

Agents coordinate through shared memory (persists across runs). Store durable findings only. Runnable examples: examples/.

Claude / MCP

pip install "atomir[mcp]" exposes atomir as an MCP server, giving Claude Desktop, Claude Code, Cursor, or any MCP client persistent memory. Add to the client's MCP config:

"mcpServers": {
  "atomir": {
    "command": "atomir-mcp",
    "env": {
      "LLM_BACKEND": "openai", "LLM_API_KEY": "sk-...",
      "EMBED_BACKEND": "openai", "EMBED_API_KEY": "sk-...", "EMBED_DIM": "1536",
      "STORE_BACKEND": "json", "STORE_PATH": "/absolute/path/atomir-memory.json"
    }
  }
}

Claude then has four tools — remember, recall, list_memories, forget — and carries memory across sessions. ATOMIR_USER namespaces the memory.

HTTP API

Run uvicorn atomir.api:app (or docker compose up). MemoryClient(url) wraps these with identical shapes.

Method Path Returns
POST /memories {user_id, text} {operations, facts}
POST /search {user_id, query, k?, decompose?} {subquestions, results}
POST /answer {user_id, query, ...} {answer, subquestions, results}
GET /memories?user_id= facts
DELETE /memories/{id}?user_id= {deleted, id}
DELETE /memories?user_id= {reset}
GET /health {status, store, llm, embedder}

Limitations

  • RECONCILE_MIN_SIM (default 0.5) is embedder-dependent — re-tune with eval/tune.py when you switch embedders.
  • JSON store: atomic writes, but single-process and rewrites the whole file — dev / small scale only; use Qdrant otherwise.
  • No multi-fact transactions; a partial add self-heals on retry (writes are per-user serialized).

License

MIT

Download files

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

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

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

atomir-0.8.3-py3-none-any.whl (99.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: atomir-0.8.3-py3-none-any.whl
  • Upload date:
  • Size: 99.2 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.8.3-py3-none-any.whl
Algorithm Hash digest
SHA256 85295e267422fdcbc1af0e3bdae6c882595ec0655920766ef332952e1fe026cd
MD5 1a9d8647050a84d223001683a6e96e98
BLAKE2b-256 515d0831e390888660759e4349b9d8de44e1c849dbce393c8ae2f343c55db7e9

See more details on using hashes here.

Release history Release notifications | RSS feed

0.8.7

2 files

0.8.6

2 files

0.8.5

2 files

0.8.4

2 files

This release

0.8.3 This release

1 file

0.8.2

1 file

0.8.1

2 files

0.8.0

2 files

0.7.0

2 files

0.6.0

2 files

0.5.2

2 files

0.5.1

2 files

0.5.0

2 files

0.4.2

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page