Настоящий движок диалога вместо EchoEngine: Engine.handle(token, event) с конвейером scrub() → safety (сначала лексикон; при ACUTE LLM не вызывается вообще) → FSM → сохранение → MonitoringService.after_data_change() → исполнение диалоговых действий. Сюда же входят каркас FSM с реестром состояний, core-flow (согласие, возраст, idle, кризис, отзыв согласия), переиспользуемая сессия шкалы и тексты в config/texts/dialog.yaml.
app/dialog/flows/ — сценарии подгружаются автоматически (load_flows()); core.py — new → consent → age_gate → <module>.onboarding.start → idle, crisis, consent_revoked.
app/dialog/scale_session.py — шкала по пунктам: кнопки; свободный текст → extract_item, при неуверенности — уточнение; п.9 PHQ-9 и C-SSRS только кнопками; флаги кризиса из конфига.
app/dialog/actions.py — исполнение schedule_scale, show_safety_plan, med_info, suggest_visit, reach_out, crisis. Алерты движок не создаёт (ADR-007).
app/dialog/crisis.py — скрипт отправляется один раз за открытый кризис. Без согласия и при недоступной БД отправляется вариант без фразы «врач уведомлён».
API для #36/#37
fromapp.dialog.fsmimportCtx,Step,on_enter,state,triggerfromapp.dialog.scale_sessionimportstart_scalefromapp.dialog.flows.coreimportfinish_onboarding@on_enter("veteran.onboarding.start")asyncdef_start(ctx:Ctx)->Step:returnawaitstart_scale(ctx,"pcptsd5",return_state="veteran.onboarding.pcl5")@on_enter("veteran.onboarding.done")asyncdef_done(ctx:Ctx)->Step:returnawaitfinish_onboarding(ctx,module="veteran")# следующий модуль или idle + приветствие@trigger("system:daily_checkin",replace=True)# подмена заглушек idle; также "button:menu:checkin", "text:idle"asyncdef_checkin(ctx,event)->Step:...
start_scale(ctx, scale_id, *, return_state, scheduled=True, intro=None) переводит FSM в scale.<id>, по завершении возвращает в return_state. Внеплановую шкалу можно поставить в очередь системным событием scale_due или действием schedule_scale. Шкала из очереди стартует, когда FSM в idle; в crisis — только C-SSRS.
contact_requests: [{"ts"}] — пациент нажал «Связаться с врачом».
consent, revoked_at, delete_requested: true — согласие отозвано, данные помечены к удалению.
Пока работает NoopMonitoring, из crisis выводит только системное событие doctor_ack. С настоящим мониторингом выход также происходит при is_in_crisis() == False.
Как проверить
make lint test → 23 новых теста в tests/test_dialog_engine.py.
Чек-лист
rebase на origin/dev
make lint + make test зелёные
лог в issue (### 2026-10-03)
нет ПДн в логах, нет реальных данных, нет секретов
общие файлы и контракты не тронуты
## Что меняется
Настоящий движок диалога вместо `EchoEngine`: `Engine.handle(token, event)` с конвейером `scrub()` → safety (сначала лексикон; при ACUTE LLM не вызывается вообще) → FSM → сохранение → `MonitoringService.after_data_change()` → исполнение диалоговых действий. Сюда же входят каркас FSM с реестром состояний, core-flow (согласие, возраст, idle, кризис, отзыв согласия), переиспользуемая сессия шкалы и тексты в `config/texts/dialog.yaml`.
Closes #10
## Зона
A
## Архитектура
- `app/dialog/engine.py` — конвейер, решения по safety, сохранение, регистрация `register_engine(Engine)` при импорте.
- `app/dialog/fsm.py` — `Ctx`, `Step(replies, next_state, data, notify_monitoring)`, реестры `@state`, `@on_enter`, `@trigger`.
- `app/dialog/flows/` — сценарии подгружаются автоматически (`load_flows()`); `core.py` — new → consent → age_gate → `<module>.onboarding.start` → idle, crisis, consent_revoked.
- `app/dialog/scale_session.py` — шкала по пунктам: кнопки; свободный текст → `extract_item`, при неуверенности — уточнение; п.9 PHQ-9 и C-SSRS только кнопками; флаги кризиса из конфига.
- `app/dialog/actions.py` — исполнение `schedule_scale`, `show_safety_plan`, `med_info`, `suggest_visit`, `reach_out`, `crisis`. Алерты движок не создаёт (ADR-007).
- `app/dialog/crisis.py` — скрипт отправляется один раз за открытый кризис. Без согласия и при недоступной БД отправляется вариант без фразы «врач уведомлён».
## API для #36/#37
```python
from app.dialog.fsm import Ctx, Step, on_enter, state, trigger
from app.dialog.scale_session import start_scale
from app.dialog.flows.core import finish_onboarding
@on_enter("veteran.onboarding.start")
async def _start(ctx: Ctx) -> Step:
return await start_scale(ctx, "pcptsd5", return_state="veteran.onboarding.pcl5")
@on_enter("veteran.onboarding.done")
async def _done(ctx: Ctx) -> Step:
return await finish_onboarding(ctx, module="veteran") # следующий модуль или idle + приветствие
@trigger("system:daily_checkin", replace=True) # подмена заглушек idle; также "button:menu:checkin", "text:idle"
async def _checkin(ctx, event) -> Step: ...
```
`start_scale(ctx, scale_id, *, return_state, scheduled=True, intro=None)` переводит FSM в `scale.<id>`, по завершении возвращает в `return_state`. Внеплановую шкалу можно поставить в очередь системным событием `scale_due` или действием `schedule_scale`. Шкала из очереди стартует, когда FSM в idle; в crisis — только C-SSRS.
## Что читает lane B (#38)
В `dialog_state.data` (без текста сообщений, только коды):
- `safety_events`: список (последние 50) `{"ts": ISO, "level": "acute"|"possible", "categories": [...], "rules": [...], "source": "lexicon"|"llm"|"both", "handled": "crisis_script"|"crisis_repeat"|"risk_screen"|"dispatched", "state": "<fsm state>"}`. После записи вызывается `after_data_change(token)`.
- `crisis`: `{"open": bool, "opened_at", "source": "safety:lexicon"|"scale:phq9"|"rule:<id>", "variant", "closed_at"?}`.
- `contact_requests`: `[{"ts"}]` — пациент нажал «Связаться с врачом».
- `consent`, `revoked_at`, `delete_requested: true` — согласие отозвано, данные помечены к удалению.
- Пока работает `NoopMonitoring`, из `crisis` выводит только системное событие `doctor_ack`. С настоящим мониторингом выход также происходит при `is_in_crisis() == False`.
## Как проверить
`make lint test` → 23 новых теста в `tests/test_dialog_engine.py`.
## Чек-лист
- [x] rebase на origin/dev
- [x] make lint + make test зелёные
- [x] лог в issue (### 2026-10-03)
- [x] нет ПДн в логах, нет реальных данных, нет секретов
- [x] общие файлы и контракты не тронуты
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Что меняется
Настоящий движок диалога вместо
EchoEngine:Engine.handle(token, event)с конвейеромscrub()→ safety (сначала лексикон; при ACUTE LLM не вызывается вообще) → FSM → сохранение →MonitoringService.after_data_change()→ исполнение диалоговых действий. Сюда же входят каркас FSM с реестром состояний, core-flow (согласие, возраст, idle, кризис, отзыв согласия), переиспользуемая сессия шкалы и тексты вconfig/texts/dialog.yaml.Closes #10
Зона
A
Архитектура
app/dialog/engine.py— конвейер, решения по safety, сохранение, регистрацияregister_engine(Engine)при импорте.app/dialog/fsm.py—Ctx,Step(replies, next_state, data, notify_monitoring), реестры@state,@on_enter,@trigger.app/dialog/flows/— сценарии подгружаются автоматически (load_flows());core.py— new → consent → age_gate →<module>.onboarding.start→ idle, crisis, consent_revoked.app/dialog/scale_session.py— шкала по пунктам: кнопки; свободный текст →extract_item, при неуверенности — уточнение; п.9 PHQ-9 и C-SSRS только кнопками; флаги кризиса из конфига.app/dialog/actions.py— исполнениеschedule_scale,show_safety_plan,med_info,suggest_visit,reach_out,crisis. Алерты движок не создаёт (ADR-007).app/dialog/crisis.py— скрипт отправляется один раз за открытый кризис. Без согласия и при недоступной БД отправляется вариант без фразы «врач уведомлён».API для #36/#37
start_scale(ctx, scale_id, *, return_state, scheduled=True, intro=None)переводит FSM вscale.<id>, по завершении возвращает вreturn_state. Внеплановую шкалу можно поставить в очередь системным событиемscale_dueили действиемschedule_scale. Шкала из очереди стартует, когда FSM в idle; в crisis — только C-SSRS.Что читает lane B (#38)
В
dialog_state.data(без текста сообщений, только коды):safety_events: список (последние 50){"ts": ISO, "level": "acute"|"possible", "categories": [...], "rules": [...], "source": "lexicon"|"llm"|"both", "handled": "crisis_script"|"crisis_repeat"|"risk_screen"|"dispatched", "state": "<fsm state>"}. После записи вызываетсяafter_data_change(token).crisis:{"open": bool, "opened_at", "source": "safety:lexicon"|"scale:phq9"|"rule:<id>", "variant", "closed_at"?}.contact_requests:[{"ts"}]— пациент нажал «Связаться с врачом».consent,revoked_at,delete_requested: true— согласие отозвано, данные помечены к удалению.NoopMonitoring, изcrisisвыводит только системное событиеdoctor_ack. С настоящим мониторингом выход также происходит приis_in_crisis() == False.Как проверить
make lint test→ 23 новых теста вtests/test_dialog_engine.py.Чек-лист