ovos-memory-plugin-local-rag: fully local, in-process RAG

July 31, 2026 · View on GitHub

LocalRAGMemory runs Retrieval-Augmented Generation entirely in-process. It loads an OVOS text-embeddings plugin and an EmbeddingsDB plugin directly. No HTTP, no cloud key. A private, offline assistant gets long-term semantic recall with nothing else running.

pip install 'ovos-memory-plugins[local-rag]'

The local-rag extra pulls the default offline stack: ovos-gguf-embeddings-plugin (labse gguf text embeddings) + ovos-chromadb-embeddings-plugin (persistent local vector store).

How it works

update_history(user, assistant)
    └─ embed("Q: <user>\nA: <assistant>")  →  db.add_embeddings(key, vec, {content, session_id})

build_conversation_context(utterance)
    ├─ q = utterance            (or recent user turns folded in, if query_mode="history")
    ├─ hits = db.query(embed(q), top_k=max_num_results)   # cosine distance
    ├─ keep hits with score >= min_score   (score = 1 - distance)
    └─ inject context per inject_mode, then recent history, then [USER: utterance]

Document ids are stable and deterministic ("<session_id>_<n>", monotonic per session). There is no randomness, so storage and tests are reproducible. Tool-call ids in inject_mode="tool" are likewise stable ("memrag_<session_id>_<n>").

Configuration

{
  "memory_module": "ovos-memory-plugin-local-rag",
  "ovos-memory-plugin-local-rag": {
    "embeddings_plugin": "ovos-gguf-embeddings-plugin",
    "embeddings_config": {"model": "labse"},
    "embeddings_db_plugin": "ovos-chromadb-embeddings-plugin",
    "embeddings_db_config": {"path": "~/.local/share/ovos/local_rag_db"},
    "collection": "ovos_local_rag",

    "retrieval": {
      "max_num_results": 5,
      "min_score": null,
      "query_mode": "utterance",
      "query_history_turns": 3
    },

    "context": {
      "header": "Use the following recalled context to answer ...",
      "chunk_prefix": "- ",
      "chunk_separator": "\n\n",
      "include_sources": false,
      "tool_name": "search_memory"
    },

    "inject_mode": "system",
    "system_prompt": "You are a helpful assistant.",
    "max_history": 10
  }
}
KeyDefaultNotes
embeddings_pluginovos-gguf-embeddings-pluginany opm.embeddings.text entry point
embeddings_config{}forwarded to the embeddings plugin constructor
embeddings_db_pluginovos-chromadb-embeddings-pluginany opm.embeddings (EmbeddingsDB) entry point
embeddings_db_config{}forwarded to the DB constructor. chromadb takes path, qdrant/base take config
collectionovos_local_ragcollection name (chromadb requires 3-512 chars from [a-zA-Z0-9._-])
retrieval.max_num_results5top-k
retrieval.min_scorenulldrop hits below this score. null keeps all
retrieval.query_modeutterancehistory folds recent user turns into the query
retrieval.query_history_turns3turns folded when query_mode="history"
context.*n/arendering of the retrieved chunk block
inject_modesystemsee the inject-modes table
system_prompt""persona base prompt
max_history10recent verbatim messages retained per session

Inject modes

All five modes from the overview are supported. The tool mode emits a synthetic assistant tool_calls turn (a search_memory call for the query) followed by a MessageRole.TOOL message carrying the chunks, just before the user utterance. The assistant-with-tool_calls turn precedes its tool result (provider ordering invariant) and the user utterance stays last. It requires a chat backend that understands tool calls.

Swapping the stack

Any embeddings + EmbeddingsDB pair works. For example, point at a shared qdrant instance by setting embeddings_db_plugin to ovos-qdrant-embeddings-plugin and supplying its embeddings_db_config. The retrieval/inject logic is backend-agnostic.

Scoring note

EmbeddingsDB.query returns a distance (lower = closer). For the cosine space used by the default chromadb stack, the plugin converts it to a similarity score = 1 - distance before applying min_score, so min_score is a similarity threshold in [0, 1] (higher = stricter).


← Long-term · Home · Lexical →