docs: bootstrap repo — CLAUDE.md, spec, ADR, TenVex-style workflow

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 14:36:33 +03:00
co-authored by Claude Opus 5.5
commit cf2cc8b5f4
15 changed files with 1671 additions and 0 deletions
+466
View File
@@ -0,0 +1,466 @@
# 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 на хакатоне (ключи от организаторов?).
- Номер и детали приказа о диспансеризации ветеранов СВО — медик из госсистемы.