Questions & Answers
The question/answer data model - how to render each question type and construct valid answers
Every turn of the conversation is: render a Question, collect a selection, send back an answer with the same type. This chapter is the reference for that contract.
Question
{
"type": "symptoms",
"id": "q-123",
"messages": [ ... ],
"choices": [ ... ],
"constraints": { "min_selections": 0, "max_selections": 4 }
}| Field | Description |
|---|---|
type | Discriminator — drives which answer type you send back and hints at the UI control (below). |
id | Question identifier (opaque; useful for analytics/logging). |
messages | Ordered display content — render all of them, in order. |
choices | The selectable options (may be absent for pure-message states). |
constraints | Selection validation the client must enforce before submitting. |
Message
The universal display unit, used in questions, legal content, and reports:
{ "value": "Have you ever been diagnosed with any of the following?",
"type": "HEADER",
"format": "PLAIN_TEXT" }type— placement hint:HEADER(title),PARAGRAPH(body text),BULLET_POINT(list item),INPUT_HINT(placeholder/help text for an input control),OTHER.format— how to interpretvalue:PLAIN_TEXT,MARKDOWN, orHTML. Render markdown/HTML safely (sanitize HTML; don't execute scripts).
For MARKDOWN, make sure your implementation correctly renders links, bolding, italics, new lines (\n) and bullet points (-).
Choice
{
"id": "C0041105",
"text": "Ringing in the ears",
"symptom_definition": {
"value": "A perception of sound (often ringing or buzzing) without an external source.",
"type": "PARAGRAPH",
"format": "PLAIN_TEXT"
}
}id— opaque identifier; echo it back inincludedorexcluded. Ids may be UMLS CUIs (C0041105), clarify codes (A290201), autocomplete ids (assessment_C2316035), duration buckets (year), or action ids — treat them all the same way.text— the label to display.symptom_definition— optional plain-language helper copy explaining the symptom; when present, show it as supporting text under the label (a standardMessage; respect itsformat). Omitted when no definition exists.
Constraints
{ "min_selections": 1, "max_selections": 1 }Enforce these client-side before enabling "Continue" (the server also validates and returns 400 on violation):
min | max | UI pattern |
|---|---|---|
| 1 | 1 | Mandatory single choice — radio buttons / tappable list |
| 0 | 1 | Optional single choice — radio + explicit skip/none |
| 0 | n>1 | Optional multi-select — checkboxes, "None of these" allowed |
| 1 | n>1 | Mandatory multi-select — checkboxes, submit locked until ≥1 selected |
Either bound may be absent — treat a missing min_selections as 0 and a missing max_selections as unlimited.
Question types and how to render them
type | Semantics | Typical UI |
|---|---|---|
generic | General-purpose question: clarifications, duration, continue/add-symptoms decisions, onboarding age/gender | Single- or multi-choice per constraints |
symptom | One symptom's conditionality ("Is the pain sharp?") | Single choice |
symptoms | "Do you have any of these?" — multi-symptom collection | Multi-select checkboxes with per-choice symptom_definition support copy |
symptoms_choose_one | Pick which of two presentations applies | Constrained choice (1–2 selections) |
factor | Influencing factors (injury, medication, pregnancy…) | Single choice |
health_background | Pre-existing conditions / medical history | Multi-select, "none apply" very common |
autocomplete | Symptom not recognised — user searches for it | Search box driven by GET /symptom-search, plus any special choices provided (add another / skip) |
Notes:
- Build one generic renderer keyed on
messages+choices+constraints, with a special case only forautocomplete(search box). New copy and choices then work without client releases. - Don't switch behaviour on specific choice ids or message text — both are content, not contract.
- The
onboarding_responsequestions returned byPOST /termsuse three additional types:numeric(age — integer input; its constraints are the valid range),choice(gender — single select), andtext(initial symptom — free text; its constraints are the character length range). These are never answered viaPOST /chat— their values are submitted together as thePOST /initialbody.
The answer
{
"type": "<same type as the question>",
"included": ["C0041105"],
"excluded": ["C0006325", "C0018681", "C0015468"]
}Rules:
typeechoes the question'stype. Sending a mismatched type is a400.included= choices the user affirmed.excluded= choices the user saw and did not select. Populatingexcludedmatters medically — an unselected symptom is evidence of absence, and the diagnostic engine uses it. Best practice: every choice id ends up in exactly one of the two lists.- At least one of the lists must be non-empty.
- Respect
constraintson the size ofincluded. - For
autocomplete,includedcarries the selected suggestion'sidfrom/symptom-search(e.g.assessment_C2316035) or a special choice id (add/skip) offered by the question.
Worked examples
Multi-select symptoms question — user has ringing in the ears, nothing else:
{ "type": "symptoms",
"included": ["C0041105"],
"excluded": ["C0006325", "C0018681", "C0015468"] }Duration (a generic question) — "about a year":
{ "type": "generic",
"included": ["year"],
"excluded": ["hour", "day", "week", "month"] }Clarification (a generic question with clarify-code ids):
{ "type": "generic",
"included": ["A290201"],
"excluded": ["A290202", "A290203"] }Health background — nothing applies:
{ "type": "health_background",
"included": [],
"excluded": ["C0011849", "C0004096", "C0010054", "C0403447"] }Autocomplete — user picked a suggestion from /symptom-search:
{ "type": "autocomplete",
"included": ["assessment_C2316035"] }Special turns without a normal answer
- Step back —
POST /step-backwith an empty body (not a/chatanswer). Gate onstep_back_possible. - Restore —
GET /chatre-reads the current question without answering. - Report —
GET /reportoncereport_possibleistrue.
