Pipeline превращает AI-сгенерированный список follow-up расписаний в напоминание (email через Mailgun, WhatsApp через WABA-org), которое срабатывает в указанный момент. Используется в patient-portal. UX-pattern (per-card vs bulk vs hybrid) — отдельно, см. follow-up-reminders-ui-pattern.
Источники данных
LLM-pipeline (followup_generation Langfuse prompt) генерирует список расписаний в shape:
days_till_followup: целое число (REQUIRED в schema по всем версиям)timeframe_description: human-readable строка («In 30 days», «In about 1 week»)purpose_description: пациент-friendly мотивацияfollow_up_tests[]: тесты этого расписания (test_name,test_purpose)
Output сохраняется в двух местах:
- Postgres
BloodTest.enrichmentDraft.followUp.followUpSchedules— все поля как есть - FHIR Healthcare API
CarePlan.activity— преобразовано в FHIR-shape
Read path
Patient-portal preferенциально читает данные из FHIR. Только если FHIR-CarePlan отсутствует — fallback на enrichmentDraft.followUp из Postgres.
patient-portal test-data.ts
→ fhirData.followUp (preferred)
→ или draft.followUp (fallback only when fhirData is null)
На проде FHIR заполнен → читается FHIR-путь. Локально (где FHIR пустой) → читается fallback.
Создание реминдера
Server-action createReminder(testId, testName, daysFromTest, description) в patient-portal:
- Auth check (current patient session)
- Verify test ownership через Prisma
- Compute
remindAt = test.testDate + daysFromTest * 86_400_000(с fallback на «завтра» если результат в прошлом) - Upsert в
PatientRemindertable — unique key(patientId, bloodTestId, testName) - Send Inngest event
reminder/scheduledкоторый sleeps доremindAt, затем доставляет notification
Inngest function reminder-send после wake-up проверяет статус (cancelled? уже sent?) и шлёт через notification channel организации.
Известные дефекты
FHIR-roundtrip-loss для days_till_followup
days_till_followup (числовое количество дней) генерируется LLM, сохраняется в Postgres enrichmentDraft, но полностью теряется в FHIR-roundtrip. На проде это приводит к тому, что click «Remind me» на любом расписании не создаёт реминдер.
Причина:
- Writer в FHIR CarePlan-builder сохраняет только текстовый
timeframe_descriptionчерезscheduledString, число дней нигде не записывается. - Reader при чтении CarePlan игнорирует extension’ы про timing, возвращает schedule без
days_till_followup.
Симптом для пользователя: расчёт remindAt получает undefined * 86_400_000 = NaN, new Date(NaN) это Invalid Date, Prisma upsert валится с PrismaClientValidationError. Server-action ловит и возвращает {success: false}. На per-card UI handler не имел catch блока — failure был silent (button оставалась в исходном состоянии без feedback). На bulk UI handler имеет catch с toast.error — failure виден. То есть выбор UX случайно влияет на видимость этого defect.
Локально дефект не воспроизводится потому что FHIR пустой и читается fallback (где число есть).
Silent client-side fail на per-card UI
Per-card версия UI вызывает server-action в try/finally без catch. Любой throw из server-action (auth/CSRF/network/Prisma) глотался без UI-feedback. Bulk версия имеет catch с toast — failure виден.
Time-of-day reminder
remindAt наследует время суток от testDate (когда сдавали кровь). Не нормализуется к утру в часовом поясе пациента — реминдер может прийти, например, в 7:42 ночи если кровь сдавали ранним утром.
Unique-key collision при bulk-set
Unique-key (patientId, bloodTestId, testName) в PatientReminder. testName в bulk-варианте берётся как schedule.follow_up_tests?.[0]?.test_name — только первый тест расписания. Если у двух расписаний первый test_name совпадает (например оба «Iron Panel» с разным days_till_followup), второй upsert перетирает первый — остаётся одна запись вместо двух.
Stale timeframe_description
Текст «In about 30 days» генерируется LLM при анализе теста и не пересчитывается при просмотре через месяц. Для старого теста становится stale — ни UI, ни pipeline не флагуют это.
Связанные решения
- follow-up-reminders-ui-pattern — status: contested — per-card pills vs bulk header vs hybrid
Открытые вопросы
- Backfill реминдеров для уже existing CarePlans без
days-till-followupextension — оставить regex-fallback наtimeframe_description, или прогнать одноразовый pipeline-rerun для пациентов с активными тестами? - Multi-language
timeframe_description: AI-pipeline может вернуть строку на греческом / польском / немецком (зависит от patient locale). Regex-fallback узнаёт только английские unit-слова (day/week/month/year). Стратегия — заставить pipeline всегда возвращать английский text + локализовать на UI-стороне, или расширять regex coverage. - Канал доставки реминдера и fallback при недоступности (нет email-verify, нет WABA-настройки) — undocumented.
Связано
- patient-portal — продукт, в котором живёт фича
- fhir-careplan — FHIR-ресурс используемый для хранения расписаний
- inngest — orchestrator реминдер-doзагрузки