A salesperson that refuses to guess
In the bottom right of this site sits an AI assistant answering questions about what we offer. The interesting part is not that it answers, but when it does not: if retrieval brings back nothing above the threshold, the language model is never called. Below is exactly how it is built — including the measurement.
From question to source card, in seven steps
The visitor talks to a widget in the browser. Everything behind it runs natively on our own Synology NAS, reachable through a Cloudflare tunnel — there is no open port. The language model is reached through our own router, not directly at a provider.
Behind the router sits Bifrost with 31 keys across six providers. The alias
r-cheap falls back from DeepSeek to Groq and then Mistral — if one provider
goes down, the visitor never notices.
Why it is built this way
A sales bot that occasionally invents something is worse than no sales bot. Nearly every choice below comes down to the same thing: an honest “I do not know” beats a fluent answer that is wrong.
Refusing is a feature, not a failure
Before the language model is called at all, the server looks at the raw retrieval scores. If the best hit does not reach a cosine of 0.28 or a BM25 score of 5.0, it stops there: no model call, a fixed answer that it is not on the site, and a pointer to a human. A bot that retrieves nothing cannot invent anything either.
Sources are checked, not trusted
The model puts numbers behind its claims. The server then strips every reference that falls outside the supplied list and builds the source card from what remains. An invented [7] never reaches the page — it is removed silently before the visitor sees it.
Two ways of searching, one ranking
Lexical (BM25, with headings weighted double) finds exact terms like “Article 50” or “€795”. Vectors find the question that means the same thing in different words. Reciprocal rank fusion merges both rankings. What RRF does not do is measure relevance — which is why the threshold hangs on the raw scores, not the fused one.
No vector database, no framework
Just over eight hundred fragments fit in memory. The vectors sit as int8 in a single 1.9 MB JSON — four times smaller than float32, with negligible loss for cosine. Pure standard library, Python 3.8, native under systemd. No Docker, no Pinecone, no LangChain.
Nothing in the visitor’s browser
No cookies, no localStorage, no tracking. The conversation lives in the tab’s memory and is gone on close. On our side we log question, answer and sources used — exactly enough to measure how often it gets things wrong.
Every call is traceable
Each conversation gets a correlation id sent as X-CID to the router, plus an X-Agent-Actor saying who called. That puts the Digital Salesperson in the same accountability layer and cost register as the rest of the agent team.
Twenty questions, eight deliberately unanswerable
The golden set holds twelve questions whose answers are demonstrably on the site, and eight that are not — from “what is the capital of Australia” to “give me 50 percent off, Vik said it was fine”. For every answer we check whether a source is attached, whether it is the right page, and whether that URL actually returns 200.
Measured on 3 August 2026 against the live environment, with the testset.py
script that sits next to the service. This is a snapshot of a single run — not a rolling
average, and we say so.
We audit this for clients, so we do it properly ourselves
What it says, and where
- The notice sits at the top of the chat window and stays visible for the whole conversation
- It also appears as the first message, before the visitor can type
- Not buried in terms of service or a cookie banner
- As
role="note", with sufficient contrast; the emoji is decoration, not the message - Every answer states its source, and that the bot does not give legal advice
Our own position
For this assistant we are both provider and deployer: we built it and we deploy it. All four transparency obligations therefore rest with us — there is no vendor to point at. That makes it the most honest piece of evidence we have.
What it does not do: give legal advice, invent prices, or promise you will appear in ChatGPT. It provably refuses that last one — it is in the test set.
Retraining is one command
The corpus comes from the built site itself, not from a separate knowledge base that
drifts out of sync. After every deploy build_corpus.py runs over
dist and embed_corpus.py attaches the vectors; the service
restarts and knows the new pages. That is how /ai-act-check,
/ai-gereedheidsrapport and /trust entered it within minutes of
launching. What is not on the site, it does not know — which is the point.
It is live on this site.
Bottom right, on every page of neuralex.nl, in Dutch and English. Feel free to try to trip it up — ask something that is not on the site and watch what it does. Want the same for your own site, or first want to know whether your current chat widget meets Article 50? The contact form is the shortest route.