Documents answer "what are the hours". They cannot answer "who has waited the longest right now", because the answer changes every minute and lives in the queue table, not in a markdown file. This part gives the staff assistant one function it may call, wires the call through the same abstraction the rest of the season uses, and then builds the guard that decides, in code, when the function is even on the table. Along the way the local model answered in Thai, which turned out to be the most useful thing it did.

The staff Assistant page. Question: Who has waited the longest right now? Answer: Priya N. has waited the longest right now, for 43 minutes. Sources: [queue]. Under the bubble the citation reads 'live queue'.
A live answer. Two real bookings were checked in through the kiosk with their check-in times set back so the waits meant something; the model called get_queue, read the result, and cited the queue rather than a document. The name is masked to a first name and an initial before the model ever sees it.

One function, described for a model

Microsoft.Extensions.AI lets an ordinary method become a tool: AIFunctionFactory wraps it, its parameters become the schema, and its description tells the model what it is for. The clinic's one function is get_queue(status), over the QueueService from season one. It returns a compact list, first name and initial, confirmation code, minutes waited and status, longest first, and nothing else: no surnames, no phone numbers. The masking happens in the service, before the data reaches the prompt, because anything in the prompt can end up in an answer. The description says, in plain words, that the function is only for questions about the current queue or waiting times.

builder.Services.AddSingleton<IChatClient>(sp =>
    new ChatClientBuilder(new OllamaApiClient(new Uri(ai.Endpoint), ai.ChatModel))
        .UseFunctionInvocation()
        .Build(sp));

The function-invocation wrapper runs the loop: the model asks for the tool, the wrapper executes it and feeds the result back, the model writes the answer. Streaming survives the loop. The answer ends with "Sources: [queue]" so the citation guard from Part 7 is satisfied, and the page renders it as "live queue".

The guard: a tool is offered, not available

Part 2's exam found the problem this section solves: offered a queue function and asked about opening hours, Llama called the function anyway. A model that reaches for a tool whenever one is present makes every answer slower and some answers stranger, and the cure is not a sterner prompt. The service classifies each question with a deterministic check for queue words, and only then adds the tool to the request; every other question is sent with no tools at all, and the log says which path was taken. The patient assistant in Part 9 never receives a tool under any classification, and the code says so in a comment where the next developer will read it.

The same page. Question: What are the opening hours? Answer: The clinic is open Monday to Saturday from 9:00 to 13:00 and 17:00 to 20:30. It is closed on Sunday and public holidays. Sources: [1]. Citation: [1] Patient information > Opening hours.
The control question. The classifier saw no queue words, offered no tool, and the answer came from the document with a normal citation. The console log for the two questions reads "tools offered for this question" and "no tools for this question".

Four things that went wrong

An extension method that would not resolve. Writing the builder's type name in full does not import the namespace its extension methods live in; the file needed a using directive. Two minutes, and a reminder that fully-qualified names are not a substitute for imports.

Part 7 had locked the door. Retrieval's rule, "nothing near enough, refuse without calling the model", is right for documents and wrong for the queue: no document is near "who has waited longest", so every queue question was refused before the tool could be offered. The tools path now skips that shortcut. Each part's sensible rule can be the next part's bug.

A test that booked someone else's slot. The new integration test seeded an appointment at a time an older test already used, and the two collided. Each test now books its own hour. Test data is shared state whether you meant it to be or not.

The Thai preamble. On one run the model, before calling the tool, wrote a sentence of narration in Thai. The page painted it as it streamed, the tool ran, the answer followed, and the guard saw a bubble that started with uncited text and replaced the whole thing with the refusal. Adding "Always answer in English" to the prompt did not stop it. The fix is in code: when the stream reports a function call or a function result, the page retracts whatever was painted before it. Models do odd things around tool calls; the application decides what the user sees.

Perishable facts, as of September 2026: Qwen 2.5 14B through Ollama's native tools API; Gemma 3 is refused tools by Ollama, and Llama 3.1 over-calls them, both per Part 2. Microsoft.Extensions.AI 10.10 and OllamaSharp 5.4; the function-invocation wrapper's name and namespace are the current ones.

Model pick: the guard-in-code decision came from the brief; the build ran on the cheaper model and was the season's longest, at twenty-five minutes and over a hundred tool uses, most of them spent on the Thai preamble.

What the AI got wrong: the local model narrated in a language nobody asked for; the coding assistant missed a using, tripped over its own earlier rule, and collided with an older test. All four are recorded above because all four will happen to you.

The meter: a tool-backed answer costs two model turns instead of one, roughly four seconds on the GPU, still well inside a receptionist's patience. The build meter for this part is the largest of the season; the retro has the number.

Checkpoint: tag private-08 in the repo. Check two patients in at the kiosk, then ask the staff assistant who has waited longest; then ask it the opening hours and confirm the console log shows no tools were offered. Next: the patient's side.