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 }
}
FieldDescription
typeDiscriminator — drives which answer type you send back and hints at the UI control (below).
idQuestion identifier (opaque; useful for analytics/logging).
messagesOrdered display content — render all of them, in order.
choicesThe selectable options (may be absent for pure-message states).
constraintsSelection 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 interpret value: PLAIN_TEXT, MARKDOWN, or HTML. 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 in included or excluded. 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 standard Message; respect its format). 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):

minmaxUI pattern
11Mandatory single choice — radio buttons / tappable list
01Optional single choice — radio + explicit skip/none
0n>1Optional multi-select — checkboxes, "None of these" allowed
1n>1Mandatory 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

typeSemanticsTypical UI
genericGeneral-purpose question: clarifications, duration, continue/add-symptoms decisions, onboarding age/genderSingle- or multi-choice per constraints
symptomOne symptom's conditionality ("Is the pain sharp?")Single choice
symptoms"Do you have any of these?" — multi-symptom collectionMulti-select checkboxes with per-choice symptom_definition support copy
symptoms_choose_onePick which of two presentations appliesConstrained choice (1–2 selections)
factorInfluencing factors (injury, medication, pregnancy…)Single choice
health_backgroundPre-existing conditions / medical historyMulti-select, "none apply" very common
autocompleteSymptom not recognised — user searches for itSearch 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 for autocomplete (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_response questions returned by POST /terms use three additional types: numeric (age — integer input; its constraints are the valid range), choice (gender — single select), and text (initial symptom — free text; its constraints are the character length range). These are never answered via POST /chat — their values are submitted together as the POST /initial body.

The answer

{
  "type": "<same type as the question>",
  "included": ["C0041105"],
  "excluded": ["C0006325", "C0018681", "C0015468"]
}

Rules:

  1. type echoes the question's type. Sending a mismatched type is a 400.
  2. included = choices the user affirmed. excluded = choices the user saw and did not select. Populating excluded matters 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.
  3. At least one of the lists must be non-empty.
  4. Respect constraints on the size of included.
  5. For autocomplete, included carries the selected suggestion's id from /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 backPOST /step-back with an empty body (not a /chat answer). Gate on step_back_possible.
  • RestoreGET /chat re-reads the current question without answering.
  • ReportGET /report once report_possible is true.