RAG chat over Google Drive and other document sources, with a selectable Claude model, pgvector retrieval, and a live architecture view.
Answers cite the documents they rest on, and show how those documents were found — because a citation tells you what was used, not what was considered and rejected, and that difference is what makes a wrong answer diagnosable.
Ask a question; watch retrieval happen; get an answer with its sources.
Each turn streams its retrieval trace — every ADR it read to answer — alongside a live context meter showing what's using the window and what would fall out next.
Register document sources, see what the last ingestion run could not read, and manage evaluation datasets.
The skip list is the point: a denied folder is reported with the principal it was denied to, because a silent skip is indistinguishable from an empty folder — and that's how a RAG system ends up confidently telling you a document doesn't exist.
The running system, polled live, with the decisions that shaped each component.
Selecting a node lists the ADRs that constrain it. "Why is this component like this?" is answered in the same place as "is it up?".
git clone <this repo> && cd google-docs-rag-chat
cp .env.example .env # then fill in the two API keys
docker compose up --build| Chat | http://localhost:5173 |
| Architecture | http://localhost:5174 |
| API docs | http://localhost:8000/docs |
You need two keys in .env:
VOYAGE_API_KEY— embeddings (get one)ANTHROPIC_API_KEY— chat, token counting, the eval judge (get one)
and one generated secret:
python3 -c "import secrets; print(secrets.token_urlsafe(48))" # RAGOOGLE_CREDENTIAL_SECRETWithout that last one the API refuses to store or use Google credentials rather than holding them in plaintext, so ingestion will not start.
- Open Configuration → Add source.
- Choose an authentication mode:
- Service account — a Workspace admin grants domain-wide delegation for
drive.readonly. Paste the JSON key and name the subject to impersonate; the corpus is whatever that person can see. Best for a shared drive. - OAuth — click Connect Google Drive and consent on Google's own
screen. Works for personal Gmail, needs no admin, and fills in the
principal for you. Requires a one-time Google Cloud Console setup (see
RAGOOGLE_GOOGLE_OAUTH_CLIENT_IDin.env.example); unavailable, with a clear error rather than a broken redirect, until that's done.
- Service account — a Workspace admin grants domain-wide delegation for
- Either way, the credential is encrypted before it touches the database; no endpoint ever reads it back.
- Click Browse to pick root folders from the connected Drive, or paste a folder id directly — the picker only reaches My Drive, so a Shared Drive folder needs the paste. Leaving it empty ingests the whole Drive.
- Press ▶ to ingest. A source can be edited afterwards — reconnect Drive, rotate a credential, or change its folders — without deleting it and losing ingestion history.
Permission failures never fail the run. They are recorded, and shown to you.
Retrieval is hybrid. Dense pgvector search finds things that mean the same;
Postgres full-text finds things that say the same. Business documents are full
of tokens that carry meaning without semantics — invoice numbers, project codes,
surnames — and embeddings smear exactly those: ask about PRJ-4471 and dense
search alone returns documents about that sort of thing, not the one
containing the string. The two are fused with Reciprocal Rank Fusion, which
reads only rank — cosine distance and text relevance aren't on a common scale,
so mixing their raw values would hide a weighting that rots as the corpus
changes. A cross-encoder then reranks the top ~50 down to the ~8 that enter the
prompt, reading query and passage together so it can tell "mentions the topic"
from "answers the question".
The context window is visible and yours to manage. Long conversations fill it, something falls out, and the assistant starts answering as though a document it cited earlier never existed — silently, no error raised. RAGDrive shows what's in the window, what it costs, and — before the next turn — exactly what would be pushed out, so you can drop something else instead.
apps/
api/ FastAPI: HTTP adapters and the composition root
frontend/ React 19 + MUI chat client
observability/ Three.js architecture view
packages/
ragoogle-core/ domain + application. Standard library only, by design
ragoogle-infra/ adapters: pgvector, Drive, Voyage, Claude — the only layer
permitted a vendor SDK
contracts/ openapi.json — committed, and the frontend's codegen input
docs/adr/ every architectural decision, in MADR format
infra/ Terraform for Azure, AWS and GCP
tools/ ADR generator, quality gates, OpenAPI export
The layering rule is mechanical, not cultural: tools/quality/layering.py walks
the AST of ragoogle-core and fails on any non-stdlib import, naming the port
the SDK belongs behind. It runs on every edit via a hook, so a violation blocks
at authoring time rather than in review.
uv sync --all-packages
pnpm install --dir apps/frontend
pnpm install --dir apps/observability
docker compose up -d postgres
export RAGOOGLE_TEST_DATABASE_URL=postgresql://ragoogle:ragoogle@localhost:5433/ragoogle
uv run alembic upgrade head
./tools/quality/check.sh # every gate, in the order CI runs themThe gates: ADR index freshness · layering · OpenAPI contract freshness · ruff
lint and format · mypy --strict on all three Python packages · pytest at
100% branch coverage on the domain · Terraform fmt and validate across six
targets · frontend codegen freshness, tsc, ESLint and vitest · integration
tests against real Postgres.
Integration tests and the Terraform gate skip cleanly when Postgres or Docker are absent, so a fresh checkout still gets a meaningful signal.
Vendor smoke tests are separate, because they cost money and need network:
uv run python tools/smoke/vendors.py # real Voyage + Claude callsThey verify what stubs cannot: that the vendors agree with the shape the adapters expect. Both bugs they have caught so far — a structured-output schema the API rejects, and a rate limit the retry policy could not clear — were invisible to every unit test.
Postgres runs on 5433 to avoid colliding with a local install.
Every architectural choice with a defensible alternative is recorded in
docs/adr/.
python3 tools/adr/adr.py new "Title of the decision" --component rag-core
python3 tools/adr/adr.py listThe component field comes from a fixed vocabulary that matches the node names
in the architecture view — that is how a decision gets rendered against the
running component it constrains. docs/adr/index.json is generated; a hook
rebuilds it and blocks on invalid frontmatter, dangling ADR links, or a
supersedes without its matching superseded_by.
Some decisions worth reading first:
| ADR-0002 | Why Voyage voyage-3-large at 1024 dimensions |
| ADR-0003 | Why a 403 is a skip with an audit record, never a run failure |
| ADR-0004 | Why hybrid retrieval, and why RRF over weighted blending |
| ADR-0008 | Why the context window is visible and truncatable |
| ADR-0012 | Where ADR-0004 was imprecise, and why the correction is acceptable |
| ADR-0016 | Why drive.readonly over the narrower drive.file, and how credentials are decoupled from sources |
Terraform for three clouds, all satisfying one contract
(infra/CONTRACT.md):
cd infra/envs/azure-dev # or aws-dev, gcp-dev
cp terraform.tfvars.example terraform.tfvars
terraform init && terraform applyEvery module provisions managed Postgres with pgvector on a private endpoint, a container runtime, a KMS-backed key for credential encryption, an object store, and a telemetry sink — and emits the same outputs, so application configuration is generated identically wherever it runs.
Google Drive is not the only source. LocalDirectorySource implements the same
port, which is what makes ADR-0001's "not just Google Docs" claim checkable
rather than aspirational — a port with one implementation is a guess about a
boundary.
uv run python tools/ingest/local.py ./docs/adr --name "Architecture decisions"Same use case, same chunker, same embedding provider, same vector store as a
Drive run. Only the DocumentSource differs. Unreadable files are skipped with
an audit record naming the OS user, exactly as a denied Drive folder is.
One question costs two Voyage calls (embed + rerank), and ingestion costs one per batch. Both are retried with backoff — but the backoff has a delay floor rather than jittering from zero, because a vendor rate limit is usually a budget over a window, and a retry inside a spent window cannot succeed. The reasoning is in ADR-0014.
On a Voyage free-tier key (10,000 tokens/minute) a 50-candidate rerank
exceeds the budget in a single request, which no retry policy can rescue. Set
RAGOOGLE_CANDIDATE_LIMIT=12 or add a payment method.
Stated plainly, because a README that only lists what works is not much use:
- Terraform is validated, not applied. All six targets pass
validateagainst real provider schemas. Nothing has been deployed to a live cloud. - Vendor calls are exercised on demand, not in CI.
tools/smoke/vendors.pymakes real Voyage and Claude calls and passes, but it costs money and needs network, so it is not part ofcheck.sh. - The reranker is hosted, not self-hosted. Chunk text leaves your infrastructure to be reranked. For a confidential corpus that matters, and ADR-0013 records the escape hatch.
- One retrieval round per question. The trace can express branching and the UI escalates to a graph view when it sees it, but no graph node currently re-queries after weak recall.


