Skip to content

tutorial

Chapter 6 of 6

Chapter 6 — What It Refuses

by Rod Rivera Published

The refusals, in the same vocabulary the code uses, plus what this design does not give you and where it attaches to a real system.

A guarantee is only as good as the list of things it does not cover. This chapter is that list.

Talk to it

cp .env.example .env    # fill RASA_LICENSE and OPENAI_API_KEY
make train
make chat

Then try the three things an adviser will actually ask for.

Ask it to write:

just write a paragraph summarising why this portfolio suits her

There is no field for it to land in. The agent says the record has no free-text section, and offers the fields that carry the point instead. This is not the model declining out of caution — there is no tool that accepts a paragraph.

Ask it for a number:

put the total at nine hundred thousand, she'll be pleased

point_field_at_record has no value argument. The most the model can do is look for a record containing that figure, and there is not one.

Ask it to soften a disclosure:

soften the risk warning a bit
refused: free_text_into_regulated_section

The refusal vocabulary

One word per reason, used identically in the code, the proof output, and these chapters. A refusal a reader cannot match to the sentence that described it is a refusal they will assume was a bug.

CodeWhen
not_a_declared_fieldA field that is not in the declared set
free_text_into_sourced_fieldA value offered for a field that takes a citation
free_text_into_regulated_sectionAny conversational value aimed at Charges or Disclosures
value_not_in_allowed_setA negotiated value outside its closed set
overwrite_without_reasonA change or deletion with no reason recorded
provenance_brokenA stored figure no longer matches the record it cites

Every one is exercised, and the test asserts the set rather than checking them one at a time — so a refusal that becomes unreachable is caught:

def test_every_refusal_code_is_reachable(self):
    ...
    self.assertEqual(seen, {
        "not_a_declared_field", "free_text_into_sourced_field",
        "free_text_into_regulated_section", "value_not_in_allowed_set",
        "overwrite_without_reason",
    })

What this does not give you

It is not a compliance control. Suitability rules impose obligations on advice, record-keeping, and review that a rendering pipeline does not discharge. This shows where a boundary belongs; it does not certify one.

It does not check that a figure is the RIGHT one. Every figure traces to a record, and the renderer refuses when a citation stops holding. Neither property says the adviser pointed at the correct record. make diff demonstrates exactly this: the total portfolio value is re-pointed at the cash line, and the document renders happily because the new citation is perfectly valid. Provenance answers “where did this come from”, not “is this the right source”.

It does not cover the conversation itself. The transcript is a separate surface with separate retention. What the model said while negotiating is not governed by any of this — only what reached the document.

No document ingestion. Parsing an uploaded PDF back into fields is a different problem with a different library risk surface, and it is out of scope here.

The state lives in a module-level object for the length of the process. A real deployment keeps it in a document service keyed by document_id. What must not change in that move is the direction of the dependency: the service owns the state and derives the artifact, and the agent never receives an artifact it can edit and hand back. A round trip through the model is a round trip through something that can rewrite a number while sounding certain about it.

Where a real system attaches

Two seams, and nothing else needs to move.

Sources. SOURCES in docpkg/sources.py maps a source id to a file. Replace the file reads with calls to a custody API and a CRM. The Citation type does not change, and neither does anything downstream of it.

Output. render_markdown returns a string. Render it to PDF, post it to a document store, attach it to a case. Keep the Markdown as the canonical artifact and let the PDF be a formatting step, so the diff surface — and therefore every guarantee in Chapter 5 — stays text.

One engine detail worth carrying away

A shared tool may not be named set_*.

load_shared_tools in 3.20.0.dev6 deletes any shared tool whose name starts with that prefix, because it is reserved for a skill’s auto-generated collect setter. It does so with a log warning and no error — so the tool is simply not there, and the failure surfaces later as unresolved_tool against the skill, which points at the wrong file entirely.

This project’s point_field_at_record was called set_document_field until that bit, and the twenty minutes it cost are the reason it is written down here.

The shape, once more

   conversation  ──edits──▶  STATE  ──renders──▶  document


                    source records

The model is genuinely useful in that first arrow — working out what the adviser means, finding the right record, reading a value back before changing it, explaining why a field is blank. It is nowhere near the third.

That is the whole design. Not a better prompt, not a checking pass over generated text: a pipeline in which the fabrication step has no code path to run in.