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 (joined → works_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).

LangChain — AtomirRetriever 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

Metadata

Release files for atomir 0.8.7

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for atomir 0.8.7
File Size Uploaded
atomir-0.8.7.tar.gz 114.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for atomir 0.8.7
File Interpreter ABI Platform
atomir-0.8.7-py3-none-any.whl Python 3 none any Details

Total release size: 224.4 kB

Release files / atomir-0.8.7.tar.gz

Download URL atomir-0.8.7.tar.gz
Size 114.2 kB
Tags Source
SHA-256 checksum
How to use checksums
5aef5b31062796dfc65cdd2251c28e3ca10e03ea58b291533be5335f70a8d2ed
BLAKE2b-256 checksum
How to use checksums
4094040708511d0c52120ca7096f56b132eac1c0e84b2523a4dbe9387277d287
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / atomir-0.8.7-py3-none-any.whl

Download URL atomir-0.8.7-py3-none-any.whl
Size 110.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e2370f903e6d8fb5e353d49ef31d93b5442295ba7556e0ab43bd68a19faef3e9
BLAKE2b-256 checksum
How to use checksums
bfac41e596b67757eae185be7cd15f8389af67864e15bb4f3338797dcc16cc9d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release history Release notifications | RSS feed

This release

0.8.7 This release

2 release files

0.8.6

2 release files

0.8.5

2 release files

0.8.4

2 release files

0.8.3

1 release file

0.8.2

1 release file

0.8.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release 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