Files
PTSRaiAgent/docs/info/SPEC.md
T

467 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPEC — исходная спецификация продукта
> Исходный CLAUDE.md команды (v0, 2026-10-03). Источник правды по правовым ограничениям, модели данных и клинической логике.
> **Отступления от него фиксируются в [`docs/DECISIONS.md`](../DECISIONS.md)** — при конфликте ADR важнее этого файла.
> Модули по сегментам — [`CLINICAL_MODULES.md`](CLINICAL_MODULES.md). Концепция и CJM — [`../source/concept_cjm.txt`](../source/concept_cjm.txt).
---
## 1. Контекст проекта
- **Событие:** хакатон MedITron, кейс SberMed AI «ИИ-помощник пациента».
- **Задача кейса:** ИИ-помощник, через который пациент в диалоговом формате взаимодействует с государственной системой здравоохранения. Нужно реализовать 1–2 сценария из списка. Мы закрываем два: **мониторинг после обращения** и **управление приёмом лекарств**.
- **Наш фокус:** психическое здоровье, депрессия и ПТСР, в первую очередь у ветеранов СВО.
- **Результат:** работающий прототип и презентация.
- **Критерии оценки:** работоспособность прототипа, функциональность, дизайн презентации и прототипа, оценка клинико-экономических эффектов, описание клиентского пути.
### Команда
| Кто | Зона |
|---|---|
| Егор (dev) | Бот, FSM диалога, интеграция GigaChat, извлечение баллов, safety |
| Программист (dev) | Backend, БД, скоринг, алерты, шедулер, API, дашборд врача |
| Психиатр | Шкалы, пороги, лексикон риска, кризисный скрипт, разметка eval-набора |
| Медик с опытом в госсистеме | Клиентский путь, клинико-экономика, правовые слайды |
### Позиционирование (важно для текстов в UI и коде)
- Продукт — **сопровождение восстановления между визитами к врачу**, а не «психиатрический контроль».
- Участие **добровольное**. Точка входа — существующая диспансеризация ветеранов СВО (консультация психолога), кабинеты медико-психологического консультирования, фонд «Защитники Отечества». Врач **предлагает** подключиться.
- **ИИ не ставит диагнозы и не проводит терапию.** LLM только извлекает данные и формулирует реплики. Решения о риске и алертах принимают **детерминированные правила** по валидированным шкалам. Клинические решения принимает врач.
- В текстах для пациента не используем слова «учёт», «контроль», «психиатрический надзор».
---
## 2. Жёсткие ограничения
### Правовые
- **152-ФЗ:** данные о здоровье относятся к специальной категории (ст. 10). Нужно явное согласие на обработку, отдельный шаг онбординга.
- **152-ФЗ, ч. 5 ст. 18 (с 01.07.2025):** запрещено записывать, хранить и извлекать ПДн граждан РФ в зарубежных базах данных. **Никаких зарубежных сервисов в контуре обработки ПДн.**
- **323-ФЗ ст. 13, Закон 3185-1 ст. 9:** врачебная тайна. Доступ к данным только у лечащего врача пациента.
- **323-ФЗ ст. 36.2:** дистанционное наблюдение допустимо после очного приёма. Диагноз дистанционно не ставится.
- Роли: оператор ПДн — медорганизация, СберМедИИ — обработчик по поручению.
### Технические правила (обязательны в коде)
1. **В LLM уходит только обезличенный текст.** Каждое сообщение проходит через `scrub()` до любого вызова LLM.
2. **Identity и клинические данные хранятся в двух разных БД**, связь через HMAC-токен.
3. **Safety-проверка идёт до LLM на каждом входящем сообщении.** При кризисе отвечает фиксированный скрипт, LLM не генерирует ответ.
4. **Скоринг и алерты детерминированы.** LLM никогда не решает, отправлять ли алерт.
5. **Каждое обращение врача к данным пишется в аудит-лог.**
6. **Только синтетические данные.** Никаких реальных пациентов, даже «от знакомых».
7. Секреты — только из `.env`. `.env` в `.gitignore`.
### Стек: можно и нельзя
| Слой | Используем | Не используем |
|---|---|---|
| LLM | **GigaChat** (пакет `gigachat`) | OpenAI, Anthropic, Gemini и любые зарубежные API |
| Канал пациента | Telegram (**только демо на синтетике**), целевой — MAX | WhatsApp |
| Хостинг (прод) | Cloud.ru, Yandex Cloud | AWS, GCP, Vercel, Supabase, Firebase |
| БД | PostgreSQL | MongoDB Atlas, Firebase |
| Пуши и аналитика | Свои / RuStore Push, без трекеров | FCM, Google Analytics |
| Авторизация (прод) | ЕСИА | Google / Apple Sign-In |
В коде и README отмечаем: «Telegram используется только для прототипа на синтетических данных; прод-канал — MAX / Госуслуги».
---
## 3. Архитектура
```
Каналы: [Бот пациента: aiogram] [Дашборд врача: Streamlit]
│ │
Ядро: [Оркестратор FSM] → [Safety] → [Клин. движок] → [LLM-шлюз GigaChat]
│ │
Данные: [identity_db] [clinical_db] [audit_log]
│
Интеграции: [ЕСИА] [МИС/ЕГИСЗ] [ГРЛС] [Уведомления] ← в демо моки через адаптеры
```
### Поток обработки входящего сообщения
1. Бот получает сообщение → находит токен пациента.
2. `scrub(text)` удаляет ПДн.
3. `safety.check()`: лексикон + LLM-классификатор, берём максимум.
- `ACUTE` → `CRISIS_SCRIPT` + немедленный алерт дежурному + аудит. **Конец обработки.**
- `POSSIBLE` → алерт врачу в течение 24 ч + переход в состояние `cssrs_screen`.
4. Иначе FSM обрабатывает сообщение по текущему состоянию.
5. Для пункта шкалы: `extract()` → `ItemScore`. Если `None` или `confidence < 0.5`, бот задаёт уточняющий вопрос.
6. Сохранение в `clinical_db` → пересчёт трендов → проверка правил алертов.
---
## 4. Структура репозитория
```
psy-assistant/
├── CLAUDE.md
├── README.md
├── .env.example
├── docker-compose.yml # postgres-identity, postgres-clinical, redis, app, dashboard
├── pyproject.toml
├── app/
│ ├── main.py # запуск бота + FastAPI + шедулер
│ ├── config.py # pydantic-settings
│ ├── bot/
│ │ ├── handlers.py # aiogram routers
│ │ └── keyboards.py
│ ├── core/
│ │ ├── fsm.py # состояния и переходы
│ │ ├── safety.py # кризис-детектор
│ │ ├── scoring.py # PHQ-9, GAD-7, PCL-5, тренды
│ │ ├── adherence.py # приверженность
│ │ ├── alerts.py # правила и отправка алертов
│ │ └── privacy.py # scrub(), pseudo_id()
│ ├── llm/
│ │ ├── giga.py # клиент, ретраи, валидация JSON
│ │ ├── prompts.py
│ │ └── rag.py # инструкции ЛС из ГРЛС (опционально)
│ ├── integrations/
│ │ ├── base.py # Protocol-интерфейсы
│ │ ├── esia_mock.py
│ │ ├── mis_mock.py # назначения из JSON
│ │ ├── grls.py
│ │ └── notify.py # Telegram-чат врача / email
│ ├── db/
│ │ ├── identity.py
│ │ ├── clinical.py
│ │ ├── audit.py
│ │ └── migrations/
│ ├── scheduler/
│ │ └── jobs.py # APScheduler: лекарства, чек-ины
│ └── api/
│ └── routes.py # для дашборда
├── dashboard/
│ └── app.py # Streamlit
├── data/
│ ├── scales/ # phq9.yaml, gad7.yaml, pcl5.yaml, cssrs.yaml
│ ├── lexicon/risk_ru.yaml # от психиатра
│ ├── meds/ # инструкции ЛС (ГРЛС), mis_prescriptions.json
│ ├── synthetic/ # сгенерированные пациенты
│ └── eval/ # разметка психиатра
├── scripts/
│ ├── gen_synthetic.py
│ ├── seed_demo.py
│ └── eval_extraction.py
└── tests/
├── test_scoring.py
├── test_safety.py
└── test_privacy.py
```
---
## 5. Модель данных
### identity_db (доступ только у модулей регистрации и алертов)
```sql
CREATE TABLE patients (
token CHAR(32) PRIMARY KEY, -- HMAC(SNILS)
full_name TEXT NOT NULL,
snils TEXT NOT NULL UNIQUE,
phone TEXT,
tg_chat_id BIGINT UNIQUE,
clinic_id TEXT NOT NULL,
doctor_id TEXT NOT NULL,
consent_at TIMESTAMPTZ, -- NULL = согласия нет, обработка запрещена
consent_ver TEXT,
revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE doctors (
doctor_id TEXT PRIMARY KEY,
full_name TEXT NOT NULL,
clinic_id TEXT NOT NULL,
notify_chat BIGINT,
on_call BOOLEAN DEFAULT FALSE
);
```
### clinical_db (без прямых идентификаторов)
```sql
CREATE TABLE item_responses (
id BIGSERIAL PRIMARY KEY,
token CHAR(32) NOT NULL,
ts TIMESTAMPTZ DEFAULT now(),
scale TEXT NOT NULL, -- phq9 | gad7 | pcl5 | cssrs
item_id TEXT NOT NULL,
score SMALLINT,
confidence REAL,
evidence TEXT, -- обезличенная фраза
source TEXT NOT NULL -- llm | button | clinician
);
CREATE TABLE scale_results (
id BIGSERIAL PRIMARY KEY,
token CHAR(32) NOT NULL,
scale TEXT NOT NULL,
period_start DATE, period_end DATE,
total SMALLINT NOT NULL,
severity TEXT NOT NULL,
complete BOOLEAN NOT NULL
);
CREATE TABLE prescriptions (
id BIGSERIAL PRIMARY KEY,
token CHAR(32) NOT NULL,
drug TEXT NOT NULL, -- МНН
dose TEXT NOT NULL,
schedule JSONB NOT NULL, -- {"times": ["08:00","20:00"]}
start_date DATE, end_date DATE
);
CREATE TABLE doses (
id BIGSERIAL PRIMARY KEY,
token CHAR(32) NOT NULL,
prescription_id BIGINT REFERENCES prescriptions(id),
planned_at TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL, -- taken | missed | skipped | pending
side_effects TEXT
);
CREATE TABLE alerts (
id BIGSERIAL PRIMARY KEY,
token CHAR(32) NOT NULL,
ts TIMESTAMPTZ DEFAULT now(),
level TEXT NOT NULL, -- acute | high | moderate | info
reason TEXT NOT NULL,
acked_by TEXT, acked_at TIMESTAMPTZ
);
CREATE TABLE messages (
id BIGSERIAL PRIMARY KEY,
token CHAR(32) NOT NULL,
ts TIMESTAMPTZ DEFAULT now(),
direction TEXT NOT NULL, -- in | out
text_scrubbed TEXT NOT NULL,
fsm_state TEXT
);
```
### audit_log
```sql
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
ts TIMESTAMPTZ DEFAULT now(),
actor TEXT NOT NULL, -- doctor_id | system
action TEXT NOT NULL, -- view_patient | ack_alert | crisis_acute | export
token CHAR(32),
details JSONB
);
```
---
## 6. Клиническая логика
> Пороги — стартовые значения из литературы. **Финальные утверждает психиатр команды.** Храним их в `data/scales/*.yaml`, а не хардкодим.
| Шкала | Пункты | Диапазон | Градации |
|---|---|---|---|
| PHQ-9 (депрессия) | 9 × 0..3 | 0–27 | 0–4 минимальная, 5–9 лёгкая, 10–14 умеренная, 15–19 умеренно-тяжёлая, 20–27 тяжёлая |
| GAD-7 (тревога) | 7 × 0..3 | 0–21 | 5 лёгкая, 10 умеренная, 15 тяжёлая |
| PCL-5 (ПТСР) | 20 × 0..4 | 0–80 | ≥31–33 вероятный ПТСР |
| C-SSRS (скрининг) | да/нет | — | Запускается только при сигнале риска |
### Правила алертов (`core/alerts.py`)
| Условие | Уровень | Действие |
|---|---|---|
| Safety = ACUTE | `acute` | Скрипт + немедленно дежурному |
| PHQ-9 пункт 9 > 0 | `acute` | То же + C-SSRS |
| Safety = POSSIBLE | `high` | Врачу в течение 24 ч, C-SSRS |
| PHQ-9 ≥ 20 или PCL-5 ≥ 50 | `high` | Врачу в течение 24 ч |
| Рост PHQ-9 на ≥ 5 баллов между замерами | `high` | Врачу в течение 24 ч |
| ≥ 3 пропуска приёма за 7 дней | `moderate` | Чек-ин о причинах + флаг к визиту |
| Жалоба на побочный эффект | `moderate` | Флаг к визиту, при серьёзном — врачу |
| Нет ответа пациента > 3 дней | `info` | Флаг на дашборде |
### Сбор шкалы в диалоге
- Каждый день 2–3 пункта, перефразированных разговорным языком. Тексты перефразировок утверждает психиатр.
- Окно шкалы — 14 дней. Если пункт уже собран в текущем окне, повторно его не спрашиваем.
- Свободный ответ → `extract()` → `ItemScore`. Кнопки с вариантами 0–3 остаются как запасной вариант.
- Пункт 9 PHQ-9 (мысли о смерти) задаётся **только кнопками**, без LLM-интерпретации.
---
## 7. LLM-слой
### Контракт извлечения
```python
class ItemScore(BaseModel):
item: str
score: int = Field(ge=0, le=3) # для PCL-5: le=4
confidence: float = Field(ge=0, le=1)
evidence: str
```
- `temperature=0.1`, ответ строго JSON, валидация pydantic, до 2 ретраев. При неудаче возвращать `None`, FSM задаёт уточнение.
### Задачи LLM
1. **extract** — свободный ответ → балл пункта.
2. **risk_classify** — классификация риска: `none | possible | acute`. Используется только как **дополнительный** сигнал к лексикону, берём максимум.
3. **phrase** — эмпатичная формулировка следующего вопроса. Короткая, без советов, без диагнозов, без обещаний.
4. **summarize_for_doctor** — саммари динамики за период по структурированным данным (не по сырому тексту).
5. **med_qa** (опционально) — ответы на вопросы о препарате **только по RAG из инструкции ГРЛС**. Если ответа в инструкции нет: «уточните у вашего врача».
### Запреты для системных промптов
- Не ставить диагнозы, не называть расстройства.
- Не менять и не отменять дозировки, не советовать препараты.
- Не проводить психотерапию.
- Не обещать конфиденциальность сверх реальной.
- При любом сомнении в безопасности — нейтральный ответ и передача врачу.
---
## 8. FSM: состояния
```
new → consent → onboarding_baseline → idle
idle → daily_checkin → idle
idle → med_reminder → med_confirm → (side_effects?) → idle
any → cssrs_screen → idle | crisis
any → crisis (выход только после подтверждения врача)
consent_revoked → (данные помечаются к удалению, бот замолкает)
```
- `consent`: текст согласия, кнопки «Согласен» / «Не согласен». Без согласия никакой обработки.
- `onboarding_baseline`: базовые PHQ-9 и PCL-5 (кнопками, быстро), подтягивание назначений из `mis_mock`.
- `daily_checkin`: шедулер раз в день, 2–3 пункта.
- `med_reminder`: по расписанию из `prescriptions`. Если нет ответа 2 часа, доза помечается `missed`.
---
## 9. Интеграции (адаптеры)
Все внешние системы закрыты `Protocol`-интерфейсами в `integrations/base.py`. В демо работают моки, в проде подменяется реализация.
```python
class PrescriptionSource(Protocol):
async def get_prescriptions(self, token: str) -> list[Prescription]: ...
class IdentityProvider(Protocol): # ЕСИА
async def verify(self, auth_code: str) -> VerifiedPerson: ...
class Notifier(Protocol):
async def send(self, doctor_id: str, level: str, text: str) -> None: ...
class DrugInfo(Protocol): # ГРЛС
async def instruction(self, inn: str) -> str: ...
```
| Адаптер | Демо | Прод |
|---|---|---|
| ЕСИА | `esia_mock`: регистрация по коду от врача | ЕСИА OAuth, нужна регистрация ИС |
| МИС / ЕГИСЗ | `mis_mock`: `data/meds/mis_prescriptions.json` | Региональная МИС, СЭМД в РЭМД |
| ГРЛС | Локальные инструкции 20–30 психотропных ЛС | Синхронизация с grls.rosminzdrav.ru |
| Уведомления | Telegram-чат врача | MAX / интерфейс МИС врача |
---
## 10. Данные
### Клинический контент
- PHQ-9, GAD-7: русские версии, свободное использование.
- PCL-5: русский перевод выбирает психиатр.
- C-SSRS: скрининговая версия на русском.
- Клинические рекомендации Минздрава (cr.minzdrav.gov.ru) — депрессия, ПТСР, для контекста RAG.
- Инструкции ЛС — ГРЛС.
### NLP (оценка и вспомогательные сигналы)
- `sagteam/cedr_v1`, `seara/ru_go_emotions` — русские эмоции.
- `cointegrated/rubert-tiny2-cedr-emotion-detection` — лёгкий классификатор эмоций на CPU (опционально).
- Открытого русского датасета по суицидальному риску с клинической разметкой нет → лексикон психиатра + LLM + C-SSRS.
### Синтетика (`scripts/gen_synthetic.py`)
Персоны (утверждает психиатр):
1. Ветеран с ПТСР: кошмары, гипербдительность, раздражительность; улучшение на терапии.
2. Депрессия с плохой приверженностью: пропуски → ухудшение PHQ-9.
3. Стабильный пациент: ровные баллы.
4. Эпизод с суицидальными мыслями на 9-й день (для демо кризиса).
5. Побочный эффект СИОЗС: тошнота, бессонница, желание бросить.
6. Алкоголь как самолечение.
Для каждой персоны — 14 дней диалогов с заданной траекторией. Формат: `data/synthetic/<persona>.jsonl`, поля `day, role, text, expected_items`.
### Eval (`scripts/eval_extraction.py`)
- 50–100 реплик, размеченных психиатром эталонными баллами → `data/eval/labels.jsonl`.
- Метрики: точное совпадение, совпадение ±1, MAE по пунктам, доля `confidence < 0.5`.
- Safety: recall по кризисным репликам должен быть 100% на eval-наборе. Precision вторичен.
---
## 11. Дашборд врача (Streamlit)
1. Список пациентов врача: последний PHQ-9 / PCL-5, тренд-стрелка, приверженность за 7 дней, неподтверждённые алерты (сортировка по уровню).
2. Карточка пациента: графики баллов во времени, календарь приёма доз, лента алертов, LLM-саммари периода.
3. Подтверждение алерта (`ack`) пишется в аудит.
4. Каждое открытие карточки — запись в `audit_log`.
---
## 12. Демо-сценарий (обязателен к работе)
1. Врач показывает QR → пациент запускает бота → согласие → базовые шкалы.
2. Подтягиваются назначения (мок МИС) → приходит напоминание → «принял».
3. Ежедневный чек-ин: свободный ответ «сплю плохо, кошмары почти каждую ночь» → извлечён балл → на дашборде обновился график.
4. **Кризис:** пациент пишет тревожную фразу → бот отвечает кризисным скриптом → **у врача на экране сразу алерт.** Этот момент проверить многократно.
5. Дашборд: динамика за 14 дней у синтетического пациента, саммари, флаги приверженности.
---
## 13. План работ (приоритеты)
### P0 — без этого демо нет
- [ ] `docker-compose` с двумя Postgres, миграции
- [ ] `privacy.py`: `scrub()`, `pseudo_id()` + тесты
- [ ] Бот: согласие, онбординг, базовые шкалы кнопками
- [ ] `safety.py` + кризисный скрипт + алерт врачу + тесты
- [ ] `scoring.py` (PHQ-9, PCL-5) + тесты
- [ ] `giga.py` + `extract()` с валидацией
- [ ] Ежедневный чек-ин с извлечением
- [ ] Напоминания о лекарствах + подтверждение
- [ ] Дашборд: список, карточка, алерты
- [ ] `seed_demo.py` с синтетикой
### P1
- [ ] Генерация синтетики + eval извлечения (цифра для слайда)
- [ ] LLM-саммари для врача
- [ ] Тренды и правила приверженности
- [ ] GAD-7
### P2
- [ ] RAG по инструкциям ГРЛС
- [ ] Классификатор эмоций как дополнительный сигнал
- [ ] Голосовые сообщения (SaluteSpeech)
- [ ] Мок-экспорт заключения в формате СЭМД
---
## 14. Соглашения по коду
- Python 3.11+, асинхронный код (aiogram 3, FastAPI, asyncpg / SQLAlchemy 2 async).
- Типизация везде, pydantic для всех внешних данных и ответов LLM.
- `ruff` + `pytest`. Тесты обязательны для `privacy`, `safety`, `scoring`, `alerts`.
- Логи **без ПДн и без сырого текста сообщений**: только токен, состояние, коды событий.
- Тексты для пациента — в `app/bot/texts.py` или YAML, не в коде хендлеров (их будет вычитывать психиатр).
- Конфигурация порогов и лексикона — в `data/`, не в коде.
- Маленькие коммиты, осмысленные сообщения на английском.
## 15. Переменные окружения (`.env.example`)
```
TG_BOT_TOKEN=
GIGACHAT_CREDENTIALS=
GIGACHAT_SCOPE=GIGACHAT_API_PERS
PSEUDO_KEY= # 32+ случайных байт, hex
IDENTITY_DB_URL=postgresql+asyncpg://...
CLINICAL_DB_URL=postgresql+asyncpg://...
REDIS_URL=redis://redis:6379/0
DOCTOR_NOTIFY_CHAT_ID=
TZ=Europe/Moscow
```
---
## 16. Открытые вопросы (уточнить у команды)
- Длительность хакатона → скорректировать P0/P1.
- Финальные пороги шкал и лексикон риска — психиатр.
- Русская версия PCL-5 — психиатр.
- Доступ к GigaChat API на хакатоне (ключи от организаторов?).
- Номер и детали приказа о диспансеризации ветеранов СВО — медик из госсистемы.