A salesperson that refuses to guess
Technical showcase: a RAG sales assistant with hybrid retrieval over 812 fragments of its own site, every source verified, and no model call at all when retrieval finds nothing. 20 out of 20 on the golden set.
Showcase · The Digital Salesperson
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.
See the architecture Go to the measurement
812
fragments from 72 of our own pages
20 / 20
test questions correct on the golden set
3.6 s
median response time, end to end
0
dead source URLs in the answers
Architecture
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.
Six choices
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.
The measurement
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.
A source present on answers inside the corpus 12 / 12
Pointed at the right page 12 / 12
Correctly refused on questions outside the corpus 8 / 8
Cited source URLs that actually return 200 100%
Median response time 3,572 ms
Fastest answer (a refusal — costs no model call) 488 ms
Slowest answer 6,088 ms
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.
Article 50
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.
Stack Python 3.8 stdlibBM25cohere/embed-v4.0int8 vectorsRRFsystemdCloudflare TunnelSentinel RouterBifrostAstro
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.
In production
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.
Go to the contact form AI Act check
Volledige (opgemaakte) versie: https://neuralex.nl/en/showcase/digitale-verkoper