1
1
Fork
You've already forked ada
0
ADA (Archived Data Assistant) is a locally hosted RAG assistant integrated into my Obsidian vault. WARNING: Contains AI generated code.
  • Python 48.8%
  • TypeScript 27.7%
  • CSS 13.5%
  • Shell 6.2%
  • JavaScript 2.3%
  • Other 1.5%
sensei a34c480ca4
improve retrieval: title boost, full-note context, overflow guard
- Title/filename token boost (TITLE_BOOST=0.10): notes are now findable
 by their own name even when hosts frontmatter doesn't alias them
- _note_body(): reconstructs the full note from its chunks in order with
 section headings restored; ranked notes now carry their whole body so
 broad questions see the complete note, not just the matching chunk
- build_context() overflow guard: if the top note alone exceeds the
 character budget it is included truncated rather than dropped, so the
 model never receives an empty context
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026年06月15日 00:40:04 -05:00
deploy add sync-box deploy configs, assembly docs, and scrub real IPs 2026年06月14日 21:34:00 -05:00
pipeline improve retrieval: title boost, full-note context, overflow guard 2026年06月15日 00:40:04 -05:00
plugin/ada Initial commit — second-brain pipeline, deploy configs, and Obsidian plugin 2026年06月14日 19:49:39 -05:00
.gitignore Initial commit — second-brain pipeline, deploy configs, and Obsidian plugin 2026年06月14日 19:49:39 -05:00
LICENSE add license 2026年06月14日 20:14:56 -05:00
README.md add scrub-work-notes.py for work-note portability 2026年06月14日 22:30:07 -05:00

ada

Source for a self-hosted RAG assistant over my Obsidian vault. Two write-ups cover how everything was built and why the decisions came out the way they did:

This repo is the code; the write-ups are the reasoning. They cover the decisions behind the architecture, the obstacles that shaped it, and the failure modes that informed each design choice.

Note on authorship: The server infrastructure — the LXC setup, Podman/Quadlet configuration, Tailscale networking, and the decisions documented in the write-ups — is my own work. The application code (Python pipeline, TypeScript plugin) was generated by AI and has not been independently audited. Read it with that in mind before deploying it anywhere you care about.

What's here

pipeline/ Python RAG service — ingestion, retrieval, generation, and HTTP API
deploy/
 sync-box/ Quadlet units for the CouchDB sync box (first write-up)
 rag-box/ Quadlet units for the indexing/API box (second write-up)
plugin/ Obsidian plugin (Ada) — sidebar UI for asking the assistant from inside the editor

pipeline/

The core pipeline, running on a CPU-only mini PC:

  • ingest.py — walks the vault, chunks each note (Markdown-aware, structure-preserving), infers source tags from the folder path (homelab / work / personal), embeds via nomic-embed-text on a local Ollama instance, and stores vectors in a DuckDB file. Incremental: only re-embeds files whose content has changed.
  • chunking.py — splits on heading boundaries, keeps fenced code blocks intact as single units, and prefixes each chunk with its note title and section so embedded chunks carry the context of where they came from.
  • retrieve.py — hybrid ranking: cosine distance via DuckDB's array_cosine_distance plus a boost for host/service/tag metadata matches. Single source of truth for result ordering — nothing else in the codebase reranks.
  • server.py — thin FastAPI wrapper around retrieve.py. Opens DuckDB read-only per request, holding the file lock for milliseconds; the ingestion job can rebuild the index at any time without contending with the API.
  • assistant.py — orchestration: retrieve locally, filter (empty notes, relevance floor, work partition excluded by default), format context, call the generation model on a remote GPU node over Tailscale, return a cited answer.
  • refresh.sh — pull-only vault sync (LiveSync CLI) followed by ingest.py. Read-only throughout; never pushes back to the vault.
  • scrub-work-notes.py — strips ## Confidential sections and <!--c-->...<!--/c--> inline spans from a folder of work notes. Run once against a copy on departure. Dry-run by default; pass --write to apply.

deploy/sync-box/

Quadlet systemd units and configuration for the CouchDB sync box (rootless Podman under a lingering user):

  • couchdb.container — CouchDB 3.5, with data and config mounted from the host. Publishes to loopback only; cloudflared reaches it over the internal network by name.
  • cloudflared.container — Cloudflare tunnel, token-authenticated. Hostname routing is configured in the Cloudflare dashboard; no local config file is needed.
  • obsidian.network — internal Podman network shared by the two containers.
  • couchdb-init.sh — run once after first start to create the CouchDB system databases. Reads credentials from a sibling couchdb.env or from the environment. Safe to re-run.
  • etc/001-livesync.ini — CouchDB configuration (CORS, auth, request limits). Mount the etc/ directory into the container at /opt/couchdb/etc/local.d/. Settings survive container recreation; credentials are not stored here.
  • couchdb.env.example / cloudflared.env.example — copy to couchdb.env / cloudflared.env (both chmod 600) and fill in credentials.

deploy/rag-box/

Quadlet systemd units for the indexing/API box (rootless Podman under a lingering user):

  • ollama.container — embedding-only Ollama instance (nomic-embed-text). No generation.
  • sb-api.container — the retrieval API, with ada.duckdb mounted read-only. Binds loopback and the box's Tailscale address; not exposed to the LAN.
  • secondbrain.network — internal Podman network so the API reaches Ollama by container name rather than through a host-published port.
  • build-livesync-cli.sh — clones vrtmrz/obsidian-livesync at a pinned tag, builds localhost/livesync-cli, and removes the clone. Run once before the first refresh.sh.

plugin/ada/

TypeScript Obsidian plugin that consumes the /ask endpoint. See plugin/ada/README.md for build, install, and configuration details.

Dependencies

Pipeline (pipeline/requirements.txt):

Package Role
duckdb vector store
fastapi HTTP API
uvicorn ASGI server
requests Ollama and generation model HTTP clients

Plugin (plugin/ada/package.json, dev dependencies only — bundled output is vanilla JS):

Package Role
obsidian plugin API types
esbuild bundler
typescript type checking

Runtime services (not in this repo):

  • Ollama on the indexing box for embeddings (nomic-embed-text)
  • Ollama on a GPU node for generation (qwen3:8b, or any model you point GEN_MODEL at)
  • The vault materialized as plain Markdown files via the LiveSync CLI (covered in the sync write-up)

Assembly order

The components must come up in this sequence. Out-of-order steps — particularly running refresh.sh before the LiveSync CLI image exists — fail with errors that don't point at the actual cause.

  1. Sync box — place the deploy/sync-box/ units under ~/.config/containers/systemd/, copy couchdb.env.examplecouchdb.env and cloudflared.env.examplecloudflared.env (both chmod 600), copy etc/ to ~/obsidian-sync/etc/, start the stack, then run couchdb-init.sh. Configure the public hostname route in the Cloudflare dashboard (couchdb.<your-domain>http://couchdb:5984).

  2. LiveSync on devices — configure the Self-hosted LiveSync plugin on each device with the tunnel URL. Confirm the vault is syncing before continuing.

  3. Indexing box LXC — stand up the unprivileged LXC with rootless Podman and Quadlet. See the RAG write-up for the uid-range, file-capabilities, and /dev/net/tun prerequisites.

  4. Pull the models:

    • On the indexing box, pull the embedding model into the Ollama container:
      podman exec ollama ollama pull nomic-embed-text
      
    • On the GPU node, pull the generation model (use podman exec <container> if Ollama runs containerized there):
      ollama pull qwen3:8b
      

    nomic-embed-text produces 768-dimensional vectors; swapping it requires rebuilding the index with a matching DIM in the pipeline.

  5. Build the LiveSync CLI imagerefresh.sh depends on localhost/livesync-cli:

    deploy/rag-box/build-livesync-cli.sh
    

    Before the first refresh, configure the CLI's settings.json to match the server's tweak values — see the RAG write-up for the required values and the remote-lock workaround. The CLI entrypoint auto-prepends the database path, so invoke it with only the command and options (e.g. --vault /vault mirror), never a leading /data argument.

  6. First refresh.sh — syncs the vault from CouchDB, mirrors it to plain files, and builds ada.duckdb from scratch.

  7. Build the API image and bring up rag-box containers. The image pins DuckDB to the host version so the container's reader matches the host's writer:

    cd pipeline/
    podman build -t localhost/secondbrain-api:latest \
     --build-arg DUCKDB_VERSION="$(python3 -c 'import duckdb; print(duckdb.__version__)')" \
     -f Containerfile .
    

    DuckDB's on-disk format is version-sensitive; the subshell reads the host version dynamically — nothing to hardcode. Then place the deploy/rag-box/ units, fill in ~/second-brain/api.env (see Configuration below), and start the stack.

  8. Disable Tailscale key expiry — in the Tailscale admin console, disable key expiry for both always-on nodes: the indexing box and the GPU node. Keys expire after ~90 days by default; when they do, the node drops off the tailnet and the assistant silently stops working. This is an admin-console setting, not a command on the box.

  9. Install the Ada plugin — build from plugin/ada/ and install into Obsidian, pointed at http://second-brain:8000 (or your node's MagicDNS name). See plugin/ada/README.md.

Configuration

pipeline/api.env.example is the template for the pipeline's runtime configuration. Copy it to ~/second-brain/api.env — outside the repo, where sb-api.container reads it — and fill in the two addresses: the local Ollama instance for embeddings (OLLAMA_URL) and the remote generation endpoint (QWEN_URL). Everything else has working defaults.

The ASSIST_MIN_SIM floor and the work-partition default exclusion are the two settings most likely to need tuning once the index is built. Both are explained in the write-up.

License

GPL-3.0-or-later. See LICENSE.