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 ollama LLM_BACKEND, LLM_API_KEY, MODEL
Embedder fake jina voyage openai gemini 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 Distribution

atomir-0.8.0.tar.gz (93.4 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.8.0-py3-none-any.whl (92.8 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for atomir-0.8.0.tar.gz
Algorithm Hash digest
SHA256 a9cd28d1a81feb3bd5e933649c8b41ccfc96eef6af2c16fd0cf17257646993d8
MD5 3235aec71d8ece6fcd0937a20d57a634
BLAKE2b-256 8117ba083f6d18b9193c3a6c918db86f6e0bdbaca26b55d09f838bec502f5a2f

See more details on using hashes here.

File details

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

File metadata

  • Download URL: atomir-0.8.0-py3-none-any.whl
  • Upload date:
  • Size: 92.8 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5bccde9b8776958395c98daccf2bc2183d19fc98f45b052778e4d2ac1e7c20c6
MD5 336b82b496aa01e16b6c6d2e45d99aaa
BLAKE2b-256 fd8c86ffd88f445d781706adbf8a10eaab30eabc2d6b56b4f38e80d690e8aa19

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

0.8.3

1 file

0.8.2

1 file

0.8.1

2 files

This release

0.8.0 This release

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