Опросник пациента для сбора medical context (диагнозы, лекарства, аллергии, симптомы, семейный анамнез, образ жизни). Реализован как multi-turn Mastra-агент в TypeScript (BG-1050, портирован из Python в session 7d777edb, 2026-03-25). Подключён к patient-portal на странице /dashboard через /api/survey route.

Использование в BloodGPT

  • Источник patient-reported данных для FHIR — наряду с health-chat и document-import. См. health-report-vision (Level 3 multi-source).
  • MedicalProfile output (текущее) — JSON свободного формата. Поля сгруппированы по доменам (medications, conditions, allergies, lifestyle).
  • Будущий FHIR output — после bridge-step (LLM нормализация → 5 smart write tools recordSymptom/recordMedication/recordAllergy/recordProcedure/recordFamilyHistory). Реализация V1.5. См. open question ниже.
  • Substitute channel для отфильтрованных полей — после Apr 27 heads sync (patient-content-filter) пациентская версия Health Status не показывает diagnoses / conditions / medications из FHIR. Опросник остаётся каналом, через который пациент сам сообщает эту информацию для интерпретации, при этом она не отображается обратно как «факт из медкарты».

Архитектура (multi-turn Mastra agent)

Не stateless wrapper и не generateObject. Каждый ход:

  • Static prompt в instructions — language requirement, update mode rules, mandatory checklist, sub-question hints, fatigue/digestive example options
  • History через messages — Mastra-агент держит conversation как массив; не template-injection в prompt
  • presentNextStep tool — единственный output mechanism. Агент вызывает с {question: "...", choiceConfig: {options: [...]}}. Frontend рендерит кнопки из options. В Mastra Studio тестируется через текстовый чат + JSON tool call.
patient enters dashboard → /api/survey?action=start
                            ↓
                       Mastra agent
                       ├─ instructions (static)
                       └─ messages (history grows)
                            ↓
                  presentNextStep tool call
                            ↓
              { question, choiceConfig: { options[] } }
                            ↓
                    frontend → buttons
                            ↓
            patient selects → /api/survey?action=answer
                            ↓
                       (loop until done)

Ключевые design decisions (из 7d777edb)

Multi-turn agent, не stateless

Ильдар: «ДА! я и говорю что это агент по сути сделанный специфично»

Initial Claude proposal: stateless wrapper с template variables ({language}, {history}, {gathered_data_json}). Ильдар отверг — конверсация natural-fit для multi-turn agent через messages. История пациента растёт через conversation-state, не через prompt injection.

Запись неуверенных упоминаний

Ильдар: «откуда статус может быть confirmed?» → решено через инструкции в промпте

Если пациент говорит “врач что-то про сахар упоминал” — записывается close-to-text:

{
  "item": "Возможно повышенный сахар",
  "details": "со слов пациента: врач упоминал, точный диагноз неизвестен"
}

Не теряем информацию через “не додумывай”; не добавляем verificationStatus поле (всё patient-reported по определению — confirmed бессмысленен). Решение на уровне промпта, не схемы.

presentNextStep как единственный UI-pattern

Не “каждый вариант ответа = отдельный tool” (вырывает control-flow из агента). Не ask_user (это фича Claude Code, не Mastra). presentNextStep со structured choiceConfig.options — frontend рендерит UI, агент содержит логику вопросов.

Восстановленный full prompt (~original)

Initial port был ~120 строк (50% от оригинала). Ильдар провёл gap analysis, попросил восстановить:

  • Language requirement (LLM забывают переводить options)
  • Update Mode фразы-примеры
  • Mandatory Checklist
  • Sub-questions для fatigue/digestive
  • Field mapping rules

Ильдар: «давай подумаем что вернуть»

Накопленные знания из итераций с реальными пациентами — эти детали не упрощать.

История 3 опросников в проекте

В session 7d777edb раскопали:

ОпросникАвторПериодАрхитектураСтатус
doctor_visit_preparationАртурлето 2025Planner + Executor (двухагентная), гипотезы по анализамНе портирован
diet_plan_generationАртурлето 2025Один агентНе портирован
medical_contextНикитадекабрь 2025Один агент, Q1-Q9Портирован в BG-1050

doctor_visit_preparation — сложнее (динамические вопросы, two-agent), отдельная фича. Не в скоупе текущего портирования.

Gotchas

  • reasoning_effort: "low" для cascade fallback на gpt-5.2 — экспериментальный параметр, не финализирован.
  • Languages: тестировался Russian + tested-paths. Hungarian, English — TODO. Существует риск что LLM забывает переводить choiceConfig.options без явной language reminder в каждом prompt-step.
  • Returning user mode в промпте есть (Update Mode rules), но не протестирован — нужен FHIR read для “что уже знаем о пациенте”.
  • gpt-5.2 для всех Mastra-агентов сейчас (см. loinc про production model config — там Gemini для production LOINC; для survey-agent только gpt-5.2 пока).

Open question — Survey → FHIR bridge (V1.5)

SurveyAgent сейчас собирает MedicalProfile JSON (свободный формат, поля сгруппированы по доменам — medications, conditions, allergies, lifestyle). FHIR-output — V1.5, через bridge-step:

SurveyAgent (V0.5)           Bridge step (V1.5, TBD)        Mastra write tools (V0.5 уже есть)
─────────────────            ───────────────────────         ──────────────────────────────────
MedicalProfile JSON   →    LLM нормализация               →   forEach(record):
{                          (term → English, role            recordSymptom(...)
  conditions: [...],        → which tool to call)            recordMedication(...)
  medications: [...],                                         recordAllergy(...)
  allergies: [...],                                           recordProcedure(...)
  lifestyle: {...}                                            recordFamilyHistory(...)
}

Открытые design-вопросы для bridge:

  • Где живёт bridge step — отдельный LLM call после survey complete? Внутри SurveyAgent как последний step? Отдельная background job?
  • Granularity нормализации — одна LLM call на весь MedicalProfile JSON (batch translate + classify) vs per-entity calls?
  • Error handling — что делать если term не нормализуется в SNOMED? Сохранять text-only с meta.tag или skip?
  • Reconciliation — если у пациента уже есть Condition с тем же кодом из chat / previous survey — update existing или create new? См. clinical-record-reconciliation.
  • Idempotency survey re-runs — пациент проходит survey второй раз; bridge step должен detect уже existing FHIR resources и не дублировать.

Эти решения зависят от того что мы будем наблюдать на реальных пациентских проходах survey — отложены до V1.5.

Связано

  • health-report-vision — Level 3 (multi-source events): survey как один из триггеров для snapshot
  • my-health — поверхность пациента, для которой опросник работает substitute-каналом
  • patient-content-filter — почему пациенту нужен substitute-канал (regulatory)
  • fhir-meta-tagging — survey-output идёт с meta.tag source: survey для filtering
  • llm-numeric-codes-policy — почему LLM в survey возвращает English term, не SNOMED код напрямую
  • clinical-record-reconciliation — что делать когда пациент повторно сообщает то же condition
  • agent-vs-workflowsurvey-agent — case где decomposed pattern НЕ подходит (open-ended conversation требует agentic loop)
  • fhir-provenance — будущий source attribution survey-данных
  • fhir-organizationOrganization/bloodgpt как author для survey-generated FHIR

Источники

Источники: 1.

Сноски

  1. Linear BG-1050, accessed 2026-05-17, https://linear.app/realai/issue/BG-1050 — issue tracking.