Dona: a local-first agentic personal assistant
Abstract
Dona answers from personal data: Google Drive documents, calendars, and a business bank account. The architecture is local-first: documents, retrieval, embeddings and vision stay on the machine through Ollama; only sensitive requests go through a dedicated branch backed by open-weights models hosted in Europe, where every write requires explicit human confirmation. Answer citations are validated by code, not by the model.
01The problem
A useful assistant has to know your documents, your calendars, your finances. Cloud assistants require sending all of that to a third party. Dona explores another path: keep as much as possible on the machine, and grant each request only the level of capability, and exposure, it actually requires.
The second problem is reliability: an assistant that makes things up is not an assistant. Every claim Dona makes must be traceable to its source, and every sensitive action must be approved before it runs.
02The architecture: one router, three branches
Every question is first routed by a small model that answers in one word. Three branches: light conversation (local model, no tools), the local agent (documents and calendars, through Ollama), and the critical agent (banking, on open-weights models hosted in Europe). When in doubt, the router picks the critical branch; if the router fails, everything falls back to the local branch, so the degraded mode still works offline.
03Ingestion and retrieval
Drive documents are transcribed to Markdown, PDFs page by page by a local vision model, then split along their headings and indexed in Chroma. Ingestion is incremental: only modified files are reprocessed, and old chunks are deleted only after the new ones are built.
Retrieval combines dense search (local embeddings) and lexical search (BM25 with French tokenization: accents, stopwords, stemming), fused through Reciprocal Rank Fusion. The agent can submit several rephrasings of the same question, each question-method pair producing its own ranking before fusion. The whole layer is deterministic: no LLM in the search path.
04The tools
- Documents: the search tool returns identified excerpts, and carries source metadata through a separate channel that never goes through the LLM.
- Google calendars (MCP): two accounts, each pinned by configuration so the model cannot query the other; event creation requires confirmation, and update and delete are kept off the whitelist.
- Banking, through the official Qonto MCP server (OAuth): 59 whitelisted tools: 31 free reads, and 28 writes that all require a human confirmation showing the real arguments of the call. The 3 deletion tools are excluded from the whitelist: the irreversible is unreachable, even with confirmation.
05Guardrails, in code
The guiding principle: never ask the model for what the code can guarantee. Citations in an answer only become sources when their marker exactly matches an excerpt actually returned by retrieval; invented markers are dropped. Tool failures are detected by code and surfaced to the user, the model never narrates its own failures. Text returned by tools is treated as data: any instructions found inside are ignored. And the models' internal reasoning is never sent to the browser.
06The stack
| Layer | Choice |
|---|---|
| Local inference | Ollama: local agent, embeddings, vision for transcription |
| Critical branch | open-weights models hosted in Europe (GDPR) |
| Agent | LangChain / LangGraph: router, three branches, middlewares |
| Index | Chroma + French BM25, RRF fusion |
| Tools | MCP: Google Calendar (2 accounts), Qonto (OAuth) |
| Interface | Flask, NDJSON streaming |
| Quality | 101 tests on hand-written fakes, no network and no LLM; interactive documentation |
07Status
Dona is open source and actively developed. The repository ships interactive documentation: a system map with drill-down to file level, and step-by-step simulations of ingestion, retrieval and answering.
@software{dona,
author = {Vinceslas, Medhy},
title = {Dona: local-first agentic personal assistant},
url = {https://github.com/Mdhvince/Dona},
note = {Open source, Myelink}
}