Part 6 put the clinic's knowledge in the database. This part makes the assistant use it: find the passages nearest to the question, hand them to the model with the question, insist on a citation, and refuse in one fixed sentence when the documents do not answer. The first-time-patient question from Part 5 gets asked again, and this time the answer is the clinic's. Along the way the guard that enforces citations threw away a perfectly good answer, which is the mistake this part is really about.

The staff Assistant page. Question: What should a first-time patient bring? Answer: A first-time patient should bring their appointment code, a photo ID, and any previous prescriptions and reports so the doctor can see their history. Sources: [1]. Beneath the bubble a small citation line: [1] Patient information > Before your appointment.
The same question as Part 5, answered from the clinic's own document, with the source shown under the bubble. No insurance card, no medication list: appointment code, photo ID, previous prescriptions, exactly what the patient information page says.

Retrieval: one query, and the audience lives in the WHERE clause

The retriever embeds the question with the same model that embedded the chunks, then asks PostgreSQL for the nearest chunks by cosine distance, joined to their documents. Two rules are enforced in that query rather than anywhere else. First, the audience: a public question may only see public documents; a staff question sees both. It is a WHERE clause on the document row, and an unknown audience fails closed to public. There is no code path where a chunk is fetched and then filtered in memory, because that is the path a future refactor forgets. Second, a distance cut-off: chunks further than 0.55 are dropped before the model sees anything, and if nothing survives, the model is not called at all; the refusal is returned directly and the log says why. Six chunks at most go into the prompt.

The prompt the model actually sees

The system prompt from Part 5 gains three sentences: answer only from the documents; end the answer with a line "Sources:" naming the numbers used; if the documents do not answer, reply with exactly "I don't know that; please ask reception." and nothing else. The user message becomes an instruction block, then "CLINIC DOCUMENTS:" with each chunk numbered and headed by its document title and heading, then "QUESTION:" and the question. The headings are the ones the chunker preserved in Part 6; without them the model would be reading "600" with no idea what it is the price of.

The guard, and the answer it threw away

The spec's rule is strict: every answer shown to a user carries a citation or the fixed refusal, never neither. The model is asked to follow it, but a model is asked, not made. So a guard in the service checks the finished text: no "Sources:" and not the refusal sentence means the shown answer becomes the refusal, and the original goes to the log with the question, where a human can decide whether the documents need a new section. Because the answer streams, the guard cannot rewrite what the user has already watched appear, so it sends one final marker delta and the page replaces the bubble's text.

The first version of the guard looked for a line starting with "Sources:". Qwen writes it inline, at the end of the last sentence: "…so the doctor can see their history. Sources: [1]". So the very first live answer, the correct one in the screenshot above, was judged uncited and shown as the refusal. Reading the log made it obvious; the fix looks for the last "Sources:" marker anywhere and reads to the end of the line, and a regression test now feeds an inline citation through the guard. This is the season's most instructive mistake so far: a rule enforced in code is only as good as the code's idea of what compliance looks like, and the model's idea was reasonable.

Three questions, three behaviours

Question: Do you accept the XYZ insurance card? Answer: The clinic does not bill insurance directly. Reception can stamp and sign a receipt for you to claim from your insurer. Sources: [2]. Citation: [2] Fees and payment > Insurance.
Meant to be the refusal screenshot, and it is not one. In Part 2 the exam sheet said nothing about insurance and every model refused. The real fees document has an Insurance section, so the retriever found it and the model answered from it, correctly. The file is still called refusal in the repo, because renaming it would hide a useful lesson: the refusal depends on what the documents say, not on the question.
Question: What is the internal rate for home sample collection? Answer: The internal rate for home sample collection through Riverside costs the clinic 150 per visit. Sources: [1]. Citation: [1] Suppliers and internal rates > Internal rates (not for patients).
A staff-only document answering a staff question. In Part 9 the patient assistant asks something similar and must get the refusal, because the audience filter never lets it see this document. That test is written now; the screen that proves it comes with the kiosk.

What the tests pin down

Three integration tests use the fake embedding generator from Part 6, so none of them needs a model. A staff chunk is returned for a staff question and not for a public one, using the chunk's own text as the question so the distance is exactly zero. An uncited answer from a fake chat client becomes the refusal. And a question whose nearest chunk is beyond the cut-off yields the refusal without the chat client being called at all, checked with a fake that throws if it is. Forty-five tests, all green.

Perishable facts, as of September 2026: the 0.55 cosine cut-off was chosen from Part 2's hit-and-miss table for nomic-embed-text and will be wrong for a different embedding model; re-measure it when the model changes. Six chunks fits a 4,096-token context with room for the answer.

Model pick: the retrieval rules, the citation contract and the guard were designed at high effort in the brief; the build ran on the cheaper model, which also found and fixed the inline-citation bug once it read the log.

What the AI got wrong: the guard's definition of a citation. The local model did nothing wrong in this part at all; every answer was grounded and cited. And one human mistake from the documents themselves: writing an Insurance section into the fees page and then expecting an insurance question to be refused.

The meter: a grounded answer costs one embedding call plus one generation with about 600 tokens of context, two seconds on the GPU. The build meter continues to accumulate for the retro; this was the longest build so far.

Checkpoint: tag private-07 in the repo. Ask the staff assistant what to bring and compare with the answer you saved in Part 5. Then ask it something the documents do not cover, such as which doctor is on duty tomorrow, and you should get the fixed sentence. Next: a live question about the queue.