A long-term, semantic memory system for Home Assistant's Assist LLM agents. Store facts, preferences, household context, and automation history that persists across conversations — and let your agents recall them through vector similarity search.
- Native LLM tools:
add_memory,search_memory, anddelete_memoryare automatically exposed to every Assist agent. - HA services:
add_memory,list_memories,search_memory,delete_memoryservices for automations and scripts, all withsupports_response=OPTIONALso you can capture the result into a script response variable. - Memory Palace: Memories are organized into Wing → Room (e.g.
household/devices,personal/preferences) so the agent — or the user — can scope recall and writes. - Layered memory (L0–L3): Identity (L0, in config), Critical (L1), Standard (L2, default),
Archive (L3). Promotion/demotion thresholds are wired in
constants.py; the background job that moves rows between L1/L2/L3 is still pending (see Roadmap). - Privacy-first scopes:
private(only the owning agent sees it) vscommon(all agents share it). Wings carry a default scope so household/personal data lands in the right place. - Local-first: All data stays on your HA instance. SQLite + WAL, embeddings cached on disk.
- Three embedding providers, graceful fallback: a native Ollama API, an
OpenAI-compatible endpoint (
/v1/embeddings— llama.cppllama-server, LM Studio, vLLM, LocalAI, Infinity, HuggingFace TEI, …), or a zero-dependency TF-IDF fallback.
- Home Assistant ≥ 2025.11.3 (tested against 2026.4.2)
- Python 3.14
- (Optional, recommended) An embedding service — either Ollama or any
OpenAI-compatible server. The default model is
bge-m3(1024-dim).all-minilm(384-dim) also works well and uses less RAM. - Without a remote service, the integration falls back to a built-in TF-IDF engine.
Add this repository as a custom repository in HACS, then install AI Memory.
- Copy the
custom_components/ai_memory/directory into your HAconfig/custom_components/. - Restart Home Assistant.
- Add the integration via Settings → Devices & Services → Add Integration.
The config flow has two entry points: the initial setup (AiMemoryConfigFlow) and reconfigure via
Configure on the integration entry (AiMemoryOptionsFlow). It is a single-instance integration.
- User
max_entries— capacity before oldest entries get evicted (default1000).embedding_engine—ollama,openai_compatible, ortfidf(no dependencies). Entries created before the provider split storedremote; on first start after the upgrade they are migrated automatically toollama(config entry version 2 → 3).
- Remote config (only for remote providers)
remote_url— base URL of the embedding service (defaulthttp://127.0.0.1:11434). For OpenAI-compatible servers give the base URL; the integration appends/v1/embeddingsitself (e.g.http://llama-server:8080).api_key— optional, sent asAuthorization: Bearer …on every request (vLLM, TEI and proxied/remote endpoints).
- Model selection (only for remote providers)
- Ollama: lists models from
/api/tags, then pulls the selected one via/api/pull(300 s timeout). - OpenAI-compatible: lists models from
/v1/models; no pull — these servers load the model at startup. - If the server cannot be reached at all, the flow returns to the connection step with
a
cannot_connecterror until the URL works. - If the server is reachable but its model list cannot be fetched, the field becomes free text so you can type the model name manually.
- Default model:
bge-m3.
- Ollama: lists models from
Reconfiguration re-uses the same step structure, so you can switch providers, change the model, or update the API key without removing the integration. Switching to TF-IDF clears the stored remote settings.
L0 identity: the earlier
identity_textstep is gone — its only consumer (LayerManager) is not wired into the LLM API yet. It will return via the options flow in a future release (see Roadmap).
| What | Where |
|---|---|
| Memory DB | <ha_config>/ai_memory.db (SQLite, WAL mode) |
| TF-IDF vocabulary | <ha_config>/.storage/ai_memory_tfidf_vocab.json (only if engine = tfidf) |
The integration owns this schema. Don't edit the DB by hand while HA is running —
WALjournaling and abusy_timeout=5000make concurrent reads safe, but a manual write while the manager holds the connection can still corrupt state.
Once installed, three tools are exposed to every Assist agent. The agent decides when to call them
based on the system prompt bundled with the integration (llm_api/prompts.py):
| Tool | Purpose |
|---|---|
add_memory |
Proactively store durable facts. Content language follows the conversation. |
search_memory |
Semantic recall before the agent answers — keeps answers consistent with stored context. |
delete_memory |
Remove outdated or wrong memories by room/wing/scope. |
add_memory:
content: "*" # required, conversation language
scope: "private" # required: private | common
summary: "" # optional, 3–5 comma-separated keywords
wing: "" # optional, English lowercase (auto-detected if blank)
room: "" # optional, English lowercase (auto-detected if blank)
search_memory:
query: "*" # required
wing: "" # optional filter
room: "" # optional filter
limit: 5 # optional, 1–20
delete_memory:
room: "" # at least one of room / wing / scope
wing: ""
scope: "" # private | commonUser: "I'm allergic to peanuts." Agent: calls
add_memory(content="User is allergic to peanuts", scope="private", wing="personal", room="health")Agent: "Got it — I'll remember the peanut allergy."
A later conversation, days later:
User: "What should I avoid when cooking for me?" Agent: calls
search_memory(query="allergies food restrictions", wing="personal")→ hits the row stored above Agent: "Avoid peanuts — you've told me about a peanut allergy."
All four services support supports_response=OPTIONAL, so you can capture the result into a script
response variable.
service: ai_memory.add_memory
data:
text: "The garage door code is 1234"
wing: personal
room: secretsThe service path always saves as
scope=common. Use the LLMadd_memorytool (or call the manager directly) if you needprivatescope from an automation.
service: ai_memory.list_memories
data:
limit: 50
wing: household
scope: common
response_variable: memoriesSupported filters: limit, wing, room, scope, agent_id.
service: ai_memory.search_memory
data:
query: "lights that should stay off after midnight"
limit: 5
min_score: 0.55
wing: automation
response_variable: resultsSupported filters: query (required), limit, min_score, wing, room, agent_id. If no
semantic match is found, the engine falls back to tokenized LIKE search and tags the result with
match_type: "text", score: 0.0.
service: ai_memory.delete_memory
data:
wing: household
room: events
scope: commonDeletes by filter (at least one of room / wing / scope). private rows can only be deleted
when the caller's agent_id matches the owner.
Memories are organized into Wings (broad categories with a default scope) and Rooms (specific topics inside a wing). Wing and room names are always English lowercase — this is a hard contract with the LLM, and unknown values are auto-created rather than rejected.
Default wings seeded on first run:
| Wing | Default scope | Default rooms |
|---|---|---|
household |
common |
devices, maintenance, events |
personal |
private |
preferences, health, secrets |
automation |
common |
routines, schedules |
general |
common |
general |
The agent is encouraged to pick a fitting wing/room but can leave them blank — the integration
auto-detects a room from content keywords (e.g. "light", "switch" → devices) and falls back
to general/general.
| Layer | Name | Where it lives |
|---|---|---|
| L0 | Identity | identity text (collection paused — returns with the L0 wiring, see Roadmap) |
| L1 | Critical | layer=1 rows, intended as the agent's standing context (wiring pending) |
| L2 | Standard | layer=2 (default). The bulk of stored memories. |
| L3 | Archive | layer=3 rows. Cold storage for rarely accessed items. |
Promotion (L2 → L1) and demotion (L1 → L3) thresholds live in constants.py
(L1_PROMOTION_THRESHOLD=10, L1_DEMOTION_DAYS=90). LayerManager.async_get_context reads the
identity text and L1 rows, but the call site that injects this into the LLM APIInstance is not
wired today — both pieces exist, they're not yet connected. Until then, every new memory is written
as layer=2 and stays there.
MemorySearch.async_search does, in order:
- SQL pre-filter —
scope='common' OR (scope='private' AND agent_id=?), plus anywing/roomfilters. - Semantic similarity — embeds the query, computes cosine similarity with NumPy against
stored vectors, keeps results above
min_score(default0.55). - Text fallback — if nothing clears the threshold, a tokenized
LIKEsearch oncontent+summaryreturns the best matches withmatch_type="text",score=0.0. - Access tracking — every returned row gets its
access_countandaccessed_atbumped, which feeds future promotion decisions.
Embedding dimension is auto-detected. Different models produce different dims (
bge-m3=1024,all-minilm=384, TF-IDF=384). The first successful embedding persists the dimension in the_metatable; subsequent reads use the cached value. Don't hardcode384.
Every successful add/delete fires an ai_memory_updated event on the HA bus. The included
sensor.ai_memory (entity id derived from the name "AI Memory"; unique_id ai_memory_store)
listens to that event and refreshes its attributes:
state: the literal string"Active"embedding_engine,max_entries,last_updatedmemory_counts:{common, private, total}layer_distribution:{L0, L1, L2, L3}countswing_distribution: count per wingpalace_structure:{wings, rooms}totals from the palace- All config-entry data (provider, model name, remote URL, …)
Use it in templates, dashboards, or to trigger automations when memory changes.
- Setup fails with
"Remote embedding service is not reachable"— the manager probes/api/version(Ollama) or/v1/models(OpenAI-compatible) during startup. Make sure the server is running at the URL you configured, or switch the provider totfidf(no probe, always works). "No embedding engine available. Please check logs."— both the requested engine and the TF-IDF fallback failed to initialize. Check the HA log for the underlying cause; usually a missing remote URL or a permissions issue writing the TF-IDF vocab file.- Model not pulled — re-enter the config flow's Model selection step; the integration pulls
models on demand via
/api/pullwith a 300 s timeout (Ollama only). First pull of a large model can take a few minutes. OpenAI-compatible servers load the model at startup — nothing to pull. - Private memories not visible — by design.
privaterows are only returned to the agent that owns them (filtered byagent_idin the SQL pre-filter). Usescope=commonfor shared facts. - Database corruption — stop HA, then delete
<ha_config>/ai_memory.db(and its-wal/-shmsiblings). You will lose all stored memories; the schema is recreated on next start. - Wing/room shows up as something unexpected — wing/room is auto-created when an unknown value
reaches the store. If the LLM persistently picks bad names, adjust
prompts.py(the system prompt) rather than trying to validate at write time.
Things that exist in code but aren't fully wired yet:
- L0/L1 context injection —
LayerManager.async_get_contextwill read the identity text (to be collected again via the options flow) and L1 rows, but the LLMAPIInstancedoesn't call it yet. Today the agent only sees memory through the three tools, not as standing context. - Layer promotion/demotion — thresholds (
L1_PROMOTION_THRESHOLD,L1_DEMOTION_DAYS) are defined; the background job that promotes high-traffic L2 rows to L1 and demotes stale L1 rows to L3 isn't implemented. New memories always land atlayer=2. - Hall/Tunnel connections —
HallTunnelManagerexists for manual cross-room linking but isn't exposed through any UI/service; automatic discovery is deferred.
custom_components/ai_memory/
├── __init__.py # setup entry + service registration
├── config_flow.py # AiMemoryConfigFlow / AiMemoryOptionsFlow
├── constants.py # DOMAIN, DB_VERSION, engine names, defaults
├── manifest.json
├── services.yaml # field selectors + descriptions for the 4 services
├── sensor.py # listens to ai_memory_updated, exposes counts
├── embedding/ # EmbeddingEngine + remote/tfidf backends
├── llm_api/ # api.py (registration), tools.py, prompts.py
├── memory/ # manager, store, search, migration, layers
└── palace/ # structure, defaults, metadata, hall_tunnel (stub)
Two paths into the same MemoryManager:
- LLM tools — registered via
homeassistant.helpers.llm.async_register_api. The agent'splatformis captured asagent_id. - HA services — same operations, exposed for automations and scripts.
Adding a new capability means updating both paths in sync: the voluptuous schema in __init__.py,
the matching selector in services.yaml, the LLM tool's parameters schema, and the system prompt
in prompts.py.
MIT © Riscue