The Phoenix auth/identity/tenancy backend repo is being renamed arcadia-app → arcadia-core (its primary OTP app is already arcadia_core). Updates prose, doc paths, and git.sky-ai.com repo URLs. Deliberately leaves the Rust crate arcadia-app-client and host arcadia-app.internal (handled separately), and the kept namespace (issuer/release "arcadia"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.5 KiB
Retrieval-Augmented Generation in arcadia-admin
This app exposes two lexical RAG surfaces to the assistant. They
share a contract (search + read) but live at different layers and
serve different content. The agent picks between them based on tool
descriptions; the operator chooses which to deploy based on corpus
shape.
At a glance
| Browser RAG | Server RAG | |
|---|---|---|
| Lib / service | @crema/lexical-rag-ui |
arcadia-search (Rust) |
| Engine | MiniSearch (BM25, JS) | Tantivy (BM25, Rust) |
| Where it runs | In the user's browser | Sibling of arcadia-core |
| Index storage | Static JSON, fetched once | mmap'd disk, ~30–80MB resident |
| Practical corpus size | ~5–10MB / ~50–100k chunks | GB-scale, no hard cap |
| Update cadence | Static — rebuilt at app build time | Live — cron, webhook, or admin trigger |
| Auth | None (bundled with the app) | JWT (via arcadia's Guardian) |
| Tool the agent calls | search_docs(query, limit) |
search_kb(query, corpus, limit?, tags?) + read_chunk(chunk_id, corpus) |
| Source content lives in | arcadia-admin/public/docs-index.json |
arcadia-search's data dir, ingested from disk or arcadia API |
| What's it best for | Static reference docs that ship with the app | Tenant-uploaded files, audit log search, anything that grows |
When the agent picks each
The system prompt in app/routes/assistant.tsx::buildAdminPreface tells
the model:
Two retrieval surfaces exist for documentation/knowledge:
search_docs(browser-side, BM25 over the bundled arcadia docs — fast, always available, small corpus) andsearch_kb(server-side, BM25 over arcadia-search — same docs ascorpus=docsfor parity, plus larger and additional corpora as the operator adds them). For questions about the bundled arcadia docs either is fine; prefersearch_kbwhen you want richer hits or when the user is asking about content that wouldn't be in the bundled docs (uploaded files, tenant-specific knowledge). Whensearch_kbreturns achunk_idyou want to expand, callread_chunk(chunk_id, corpus).
In practice DeepSeek + V3 picks search_kb for anything that mentions
"the kb" or sounds dynamic, and search_docs for quick lookups against
the bundled docs. Neither pick is wrong for content that exists in
both.
Browser RAG (@crema/lexical-rag-ui)
What it is. A small React + MiniSearch wrapper. The lib provides
RAGProvider, useRAG, and a headless createRAGClient(indexUrl).
The index is a single JSON file built offline by
scripts/build-docs-index.mjs and shipped in the app's public/.
How it's wired here.
- Build script:
arcadia-admin/scripts/build-docs-index.mjsreads markdown from../reference/arcadia-core/, chunks at H1–H3, producespublic/docs-index.json. Runs onnpm run build:docs(and as theprebuildstep beforenpm run build). - Tool wrapper:
app/lib/admin-tools.tsconstructs a singletoncreateRAGClient("/docs-index.json")and exposes it as thesearch_docstool. The tool returns hits with the legacycategoryfield collapsed back fromtags[0]so the agent's prior expectations stay stable. - Storage: just the static JSON. No state, no auth, no indexer process.
Limits. Practical ceiling is ~5–10MB index. Past that, first-load parse and browser memory get painful (200MB+ heap on a 50MB index; mobile breaks). Updates require a build step + redeploy.
Why it exists. Static reference content that ships with the app — arcadia's own docs, in this case. Always available even if the search service is down. Zero infrastructure.
For the lib itself see lib-lexical-rag-ui/README.md.
Server RAG (arcadia-search)
What it is. A standalone Rust HTTP service (Tantivy + axum). Single static binary, ~30–80MB resident. Per-tenant per-corpus indexes on disk. JWT auth, HMAC webhook intake, atomic rebuild swap, systemd timer cron.
How it's wired here.
- Tools:
app/lib/admin-tools.tsexposessearch_kbandread_chunk. The fetch URL isKB_BASE_URL(defaulthttp://127.0.0.1:7800, override viawindow.__ARCADIA_SEARCH_URLorVITE_ARCADIA_SEARCH_URL). The bearer token is the user's arcadia JWT fromsessionStorage["arcadia_access_token"], with a"dev"fallback when no login. - Reindex button:
app/routes/ai.tsx::reindexKBcallsPOST /index/:corpus/buildand toasts the result. Lives in the AI page's empty state next to the block-preview button. - System prompt: see the snippet in
assistant.tsx::buildAdminPrefaceabove.
Storage. <INDEX_DIR>/<tenant>/<corpus>/current/ per index;
previous-<stamp>/ for the last few rebuilds (rollback). Sources can
be on-disk markdown, or pulled from arcadia's /api/v1/digital_objects
API (see arcadia-search/MULTI_TENANT.md and ARCADIA_INTEGRATION.md).
Update cadence. Three triggers, layered so each compensates for the others' failure modes:
- Cron (systemd timer, hourly default) — always-on safety net.
- Admin button — one-click rebuild from the AI page.
- Webhook — arcadia POSTs
/events/changedon file create/delete; search debounces (2-min default) and rebuilds.
Why it exists. Anything that doesn't fit the browser ceiling: tenant-uploaded files, audit-log-ish content, multi-tenant knowledge bases, anything that grows over time.
For the service see arcadia-search/README.md.
For multi-tenant config see arcadia-search/MULTI_TENANT.md.
For the upstream arcadia integration story (file content fetch,
text extraction, webhook signature, service tokens) see
arcadia-search/ARCADIA_INTEGRATION.md.
How they coexist
The default deploy runs both:
search_docsindexes the same arcadia-core docs the parity corpus onarcadia-searchindexes. Same content, two engines.- This is intentional — it means the assistant always has something
to search, even if
arcadia-searchis down or unreachable. The failure mode is "nosearch_kb, butsearch_docsstill works." - It also gives a permanent A/B regression test: query both, compare hits, catch relevance regressions in either engine.
Picking ONE for a new corpus
Use this checklist when adding new content:
| Question | Answer → use |
|---|---|
| Is the corpus < 5MB and basically static? | Browser |
| Does it need to update without a redeploy? | Server |
| Is it per-tenant content (uploaded files, tenant-specific KB)? | Server |
| Are you OK shipping it in the JS bundle? | Browser |
Does it need agentic read_chunk follow-up? |
Server (browser doesn't expose read over the tool surface) |
| Does it need to work offline / with no backend? | Browser |
| Is it growing > 10MB? | Server |
Most "knowledge base" content lives in the server side. The browser side is reserved for the always-bundled reference material that ships with the app.
What lives where (cheat sheet)
| Want to | Look at |
|---|---|
| Add a doc to the bundled browser RAG | arcadia-admin/scripts/build-docs-index.mjs (extend SOURCES) |
| Add a tool to the agent | arcadia-admin/app/lib/admin-tools.ts |
| Change the LLM's tool-picking guidance | arcadia-admin/app/routes/assistant.tsx::buildAdminPreface |
| Add a corpus to arcadia-search | arcadia-search/deploy/<tenant>/<corpus>.config.json + new systemd timer |
| Add a tenant to arcadia-search | arcadia-search/MULTI_TENANT.md |
| Wire an arcadia file → search ingest | arcadia-search/ARCADIA_INTEGRATION.md (needs upstream changes first) |
| Reindex the server-side corpus right now | "reindex kb (docs)" button on /ai empty state, or POST /index/docs/build |