AI assistant over a company's internal data: a RAG bot in Telegram with access tiers
The bot answers questions across internal chats, email, voice notes and documents — always with a citation back to the source. Search was rewritten from in-memory BM25 to SQLite FTS5: memory dropped 20×, which let the bot move off a contractor's laptop onto a server, where it now answers around the clock.
Everything the business knew lived in chat threads
A US outdoor-gear brand, two Shopify stores, a distributed team: the business owner, ops people, an ad manager, contractors. No system of record for decisions. Everything lived in working chats: tens of thousands of messages, voice notes instead of documents, forwarded PDFs and screenshots, plus a mail corpus of more than 100,000 messages.
Answering a question like "what did we decide about this two weeks ago" cost half an hour of digging: scroll the thread, remember which thread it even was, find the voice note, listen to all of it. Some decisions were simply lost — they got re-asked and re-made.
The owner's requirement was blunt: "it matters that the knowledge base has as much data as possible." But those same chats held personal correspondence and PII that the team must not see. So the job was never "bolt on RAG" — it was "bolt on RAG without turning completeness into a leak."
A full RAG pipeline on the company's own data — privacy enforced before the index
The pipeline runs one way: raw material is collected, converted to text, stripped of secrets, indexed behind an ACL, and only then does the LLM synthesize an answer. Privacy is enforced on the way in, not filtered on the way out — so it can't be talked around with a cleverly worded question.
Chats pulled per lane and merged by message id, plus the mail corpus and forwarded documents
whisper for voice notes, OCR for screenshots — everything becomes text with a link back to the source
Card numbers, CVVs and passwords are stripped before writing to the index, not at answer time
SQLite FTS5 as the postings store, a custom ranker, access tiers layered over sources
LLM synthesis with mandatory citations, delivered in Telegram
Collection: merge by id, not rsync
Chats are pulled in lanes and merged by message id. That's not pedantry: a naive copy-over (the rsync approach) would wipe history on the first partial pull — the source returns the last N messages, not everything. Voice notes go through whisper, screenshots through OCR, forwarded documents are indexed as their own sources. Everything ends up as text, and every chunk keeps a link back.
Access tiers with a single source of truth
Three tiers: owner — team — hub. They are implemented as an ACL over sources, not over answers. And here's the part that actually matters: the bot isn't the only consumer of the base — a fact-distillation job reads it too, and its filter is separate code that will happily drift away from the bot's ACL.
So both sides call the same
is_private_source
function, and a guard test watches for drift: if someone introduces a second copy of the privacy logic,
CI breaks instead of privacy.
Search: in-memory BM25 → SQLite FTS5
The first version kept the whole index in process memory. That was fine while the bot lived on a laptop.
I moved storage to SQLite FTS5 with one deliberate twist: FTS5
is used purely as a postings store (via
fts5vocab),
while ranking stays custom.
The reason is concrete: FTS5's built-in
bm25()
returns one scalar per document and exposes no per-term information — which is exactly what the coverage
factor needs (how much of the query a document actually covers, versus scoring high on one frequent word).
Handing ranking to the engine would have meant worse results.
The migration was made risk-free: the engine is switched by a single environment variable, old and new were run against the same queries, and the output was compared rank for rank. No differences — only the cost changed.
What got built on top of the base
The knowledge base turned out to be a platform, not a feature. Living on top of it:
- Fact distillation — correspondence is collapsed into structured facts rather than a retelling
- Morning digests and boards in a Telegram hub: business pulse, campaign pulse, infra pulse, tracker digest
- About twenty watchdogs on a "if it breaks, DM me and post to Discord" principle: CRM liveness, stuck orders, the chargeback protection window, an important message from the owner, index staleness
- An MCP server over the base — the owner's personal models see the same knowledge base, with no second pipeline
No vector database — SQLite and a custom ranker
The core: collection, indexing, ranking, delivery — one language across the pipeline
Postings store; ranking stays custom for the sake of the coverage factor
Per-term weights plus query coverage; engine switched by a single ENV variable
Answers are assembled only from retrieved chunks, citations are mandatory
Voice notes and screenshots become first-class sources instead of black boxes
A DM bot plus a forum hub with topics for digests and alerts
The same knowledge base is available to the owner's personal models
Services, timers, ~20 watchdogs alerting into a channel
70 test files, ~845 tests — including the guard test against ACL drift
153 Python modules, roughly 36,800 lines, 318 commits. The load-bearing pieces: the index engine (~1,300 lines), the bot (~1,500), the secret redactor (~560), distillation (~430), a stdlib-only Telegram delivery layer, the MCP server, the lane collector, and ~20 watchdog modules.
The same results at a different price
20× lower — with identical results
output compared rank for rank, no differences
measured on the live index, before and after the ACL fix
The bot moved off the laptop
The 20× memory drop wasn't a benchmark for its own sake. It's precisely what allowed moving the bot off a contractor's laptop onto a server with just 961 MB of RAM. The chronic "the laptop went to sleep, so the bot is silent at night" problem is gone at the root: it answers around the clock instead of only while someone's lid is open.
Privacy: measured, not assumed
You can argue about ACLs in the abstract for a long time. Instead I measured it on the live index: how many private chunks does the "team" tier actually reach? The answer was unpleasant — 1327.
After the ACL fix, the volume visible to that tier shrank from 6139 to 4560 chunks, of which private ones now number 0 — and no working data was lost. Separately, the access log was checked: it confirmed that no one had actually reached those chunks in the meantime.
What changed day to day
"What did we decide about this" is now a question to the bot, and it comes back with citations to specific messages — including ones that were originally voice notes. Morning digests arrive on their own, watchdogs speak up before a customer notices the problem, and the owner's personal models work against the same base through MCP — no second pipeline, no second copy of the data.
Where else the same methodology applies
This case isn't "a chatbot for chats". It's the standard problem of "company knowledge lives in unstructured conversation, and some of it must not be shown". Nearly every team older than a year has it:
- → Support and sales — customer conversation history as an answer base, without personal data spilling into general access
- → Agencies and studios — calls, briefs and client voice notes made searchable with a citation, instead of "I think we agreed on this"
- → Field services and construction — job requests, site photos, foreman chats; a cited answer replaces a round of phone calls
- → Onboarding new hires — the "team" tier gives working context without opening management's private correspondence
- → Companies restricted from the cloud — everything but synthesis runs on your own machine: a SQLite file, not an external vector service
- A collection pipeline that merges by id — history survives a partial pull from the source
- Secret redaction before the index, plus one source of truth for privacy with a guard test against drift
- The FTS5-as-storage plus custom-ranker pattern: cheap memory without giving up result quality
- Engine switching via ENV plus rank-for-rank output comparison — migration without taking anything on faith
- A watchdog and morning-digest layer over the base — knowledge starts arriving on its own, not only on request
If your company's knowledge sits in chats and inboxes and opening it to everyone is scary — that's solvable
We start with one source and one access tier — a working bot with citations shows up long before the complete base does. Email, voice notes, documents and watchdogs get added after that.
The 5,000 ₽ audit — with a concrete report and quote
I'll tell you what to deploy in your business first, what the payback looks like, and whether you need AI for the task at all (sometimes you don't).
Or just send your question — I reply within 2 hours