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.

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.

BROWSER Chat widget Article 50 notice always visible no cookies · no storage CLOUDFLARE Tunnel verkoper.neuralex.nl no open port OWN HARDWARE — SYNOLOGY NAS Digital Salesperson · systemd :8100 Python 3.8 stdlib · no Docker · no framework rate limited · 600 characters per question Retrieval BM25 + cosine fused with RRF (k=60) top 6 fragments Corpus 812 fragments · 72 pages int8 vectors, 1,536 dim 1.9 MB in memory Threshold cosine ≥ 0.28 or BM25 ≥ 5.0 below that: no model call at all Sentinel Router :8093 alias r-cheap + fallback chain X-CID · X-Agent-Actor Fixed answer “that is not on our site” handed to a human · 488 ms

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.