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 Distribution

atomir-0.8.5.tar.gz (107.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.8.5-py3-none-any.whl (103.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: atomir-0.8.5.tar.gz
  • Upload date:
  • Size: 107.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.8.5.tar.gz
Algorithm Hash digest
SHA256 e011f57c680e201826e45714a06a08fb45fcc26793922c42b72699bcd0c82e82
MD5 050cefdb9a0a625d87897cf186db0096
BLAKE2b-256 83ea2ba71d8db37dbea2a530ba706735206eed6249c51197fe79bbbaa2fa66bd

See more details on using hashes here.

File details

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

File metadata

  • Download URL: atomir-0.8.5-py3-none-any.whl
  • Upload date:
  • Size: 103.4 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.5-py3-none-any.whl
Algorithm Hash digest
SHA256 5b1755602ea387062c03032579f59f9d40f838c6e24e24313aa6e07ce6602d09
MD5 2d0d4f6511ec32536326b26fca820a05
BLAKE2b-256 73a3c2f3d6d963f4f0c2b30f3a2d3fd5a4ce72e6639527ce319f664a31c518e7

See more details on using hashes here.

Release history Release notifications | RSS feed

0.8.7

2 files

0.8.6

2 files

This release

0.8.5 This release

2 files

0.8.4

2 files

0.8.3

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