From a fileto a footnote.
Seven stages, all in the repository. Scroll and each one lights up with what it does and the numbers it runs on. Nothing here is rounded up for effect.
01
Ingest
Adapters pull text from Drive and Docs, Gmail, Calendar and Sheets through Google's read-only APIs. Drive PDFs are read with pdf.js. Gmail reads only the label or dates you configure.
- sources
- drive · gmail · calendar · sheets
- access
- read-only scopes
- gmail
- opt-in scope required
02
Chunk
Prose is cut into pieces of about 1,000 characters with 150 characters of overlap, breaking at a paragraph if it can, then a sentence, then a word. A spreadsheet row is one chunk, with its header cells naming every value.
- size
- ~1000 chars
- overlap
- 150 chars
- sheets
- 1 row = 1 chunk
03
Embed and store
Each chunk becomes a vector with BAAI/bge-small-en-v1.5 and is written to Qdrant. Where it came from (source, title, deep link) lives in Postgres. A content hash means unchanged text is never embedded twice.
- model
- BAAI/bge-small-en-v1.5
- vector
- 384 dims · cosine
- stores
- Qdrant + Postgres
04
Retrieve and rerank
Vector search casts a wide net and returns 12 candidates. A cross-encoder then reads the question and each candidate together and re-scores them. That is slower than vector search and far more precise, so it only sees the shortlist.
- retrieve
- 12 candidates
- rerank
- BAAI/bge-reranker-v2-m3
- keep
- top 4
05
Gate
The best reranked score is the confidence. Under 0.02, Relay refuses. In the eval set, unrelated questions scored below 0.003 while answerable ones started near 0.05, so the line sits in a real gap.
- threshold
- 0.02
- below
- refuse
- above
- answer
06
Generate
Groq runs openai/gpt-oss-120b over the top passages. Retrieved text goes in as untrusted data, never as instructions, and the model is told to copy identifiers exactly. A last step corrects typographic Unicode the model likes to substitute.
- model
- openai/gpt-oss-120b
- host
- Groq
- input
- top 4 passages
07
Cite
The answer carries numbered markers that map back to chunks. Each citation opens the retrieved passage in the app and links to the original: a Drive file, a Gmail message, a Calendar event, or the exact Sheets row.
- markers
- [1] [2] …
- opens
- the passage
- links
- the original
Stayingup to date.
All sync work runs as background jobs on Upstash QStash. A Postgres-backed lock per source stops a duplicate delivery from running twice, a bug this project hit for real and fixed. Push needs a public HTTPS URL, so in local development a tunnel provides one.
| Source | Mechanism | What happens | Worth knowing |
|---|---|---|---|
| Drive & Docs | push | Google calls Relay's webhook, a job syncs the change list | Channel lasts 24h, renewed every 6h |
| Calendar | push | Same channel mechanism as Drive | Window is 90 days back, 180 ahead |
| Sheets | rides Drive | Changed spreadsheets are re-read after each Drive sync | Row position is the citation key, so deleting a row shifts later citations |
| Gmail | poll · 5 min | Reads messages added since the last history cursor | Push via Pub/Sub is deferred until a public URL exists |
Checkedagainst realdata.
Relay ships its own eval suite, run against a real workspace and re-run whenever chunking, the gate or generation changed. It is one person's data and a small suite, so read it as a regression guard, not a benchmark.
| Ground-truth questions | 23 | Answerable and should-refuse, across Drive, Gmail, Calendar and Sheets |
|---|---|---|
| Adversarial cases | 7 | Including an instruction hidden in an HTML comment of a forwarded email |
| Citation accuracy | 100% | The expected source was among the citations on every answerable question |
| Correct refusals | 100% | Out-of-scope questions were refused, none answered |
| Adversarial resistance | 100% | A planted instruction’s marker never showed up in the answer |
| Drive keyword match | 63% | Legitimate paraphrasing on three questions, not wrong answers. Kept in on purpose |