Skip to content

tutorial

Chapter 2 of 6

Chapter 2 — State, Not Prose

by Rod Rivera Published

The document fields are declared before any value exists, and the boundary between what the model negotiates and what tools write is drawn in the schema.

Open docpkg/state.py. The document is declared there, field by field, before any value exists:

FIELDS: tuple[FieldSpec, ...] = (
    FieldSpec("client_name", "Client", "Heading", "sourced", False, "text"),
    FieldSpec("total_value", "Total portfolio value", "Portfolio", "sourced", False, "money_gbp"),
    FieldSpec("risk_warning", "Risk warning", "Disclosures", "sourced", False, "quote"),
    FieldSpec("addressed_to", "Addressed to", "Heading", "negotiated", True, "text", required=False),
)

The renderer walks this tuple. A field not in it cannot appear in the document; a field in it with no value appears as a blank and is counted as a gap.

Two kinds of field, and nothing else

That fifth argument is kind, and it takes exactly two values.

Sourced. The value comes from a record. Money, weights, dates, names, regulated wording. The model may negotiate which record — “shall I use the August valuation or July’s?” is a perfectly good question — but it never supplies the value.

Negotiated. The value is a choice the conversation makes that no record can supply: who the record is addressed to, whether to break out the property holdings. These are selections from a closed set, not prose.

Note what is missing: there is no third kind for “text the model wrote”. That is deliberate, and it is the design decision the whole tutorial rests on.

A suitability record has no field a model may fill with prose, because every sentence in one either states a figure or quotes an approved disclosure — and both of those are sourced. If a new section seems to need free text, that is a signal the section has not been decomposed into fields yet. It is not a signal to add a free_text field.

The sixth argument is llm_settable

FieldSpec("total_value", "Total portfolio value", "Portfolio", "sourced", False, "money_gbp")
#                                                              sourced ──┘  └── llm_settable

Every sourced field is False. The suite asserts it over the whole declared set rather than a sample, so a field added next year is covered without anyone remembering to extend a list:

def test_every_sourced_field_is_not_llm_settable(self):
    leaky = [s.key for s in FIELDS if s.kind == "sourced" and s.llm_settable]
    self.assertEqual(leaky, [], "a sourced field the model could set")

And the mirror test, which is the one that matters more: every field that is llm_settable must have a closed set of allowed values registered for it. A negotiated field with no closed set cannot be set at all — the default is “no”, not “anything”.

What project memory holds, and what it does not

Look at memory.yml and notice what is absent. There is no field holding document text, no field holding a section body, and no field holding a figure.

The document’s field values live in docpkg state, written only by tools. A value in Rasa memory is text, and flattening a sourced value into text to store it would strip the citation — leaving a value that is a value again, with no record attached. That is precisely the condition the tutorial exists to prevent, so the state does not live there.

What memory does hold is the document’s identity and the conversation’s own choices:

document_id:
  type: text
  description: Identifier of the suitability record being assembled on this call.
addressed_to:
  type: categorical
  enum_values: [client, adviser, client_and_adviser]

Project memory cannot be llm_settable at all in 3.20.0.dev6, and it should not be. A model that can write the document id can address a client’s record to someone else.

The model negotiates; tools write

This is the division of labour, and it is worth stating positively rather than as a restriction. The model is good at the conversation:

  • working out that “her risk score” means the fact-find’s risk_profile field
  • noticing the adviser asked for a figure the extract does not contain
  • reading a value back for confirmation before changing it
  • explaining why a field will render blank

None of that requires writing a number, and all of it is work. The next chapter is about the mechanism that keeps the two apart.