# 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/.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 на хакатоне (ключи от организаторов?). - Номер и детали приказа о диспансеризации ветеранов СВО — медик из госсистемы.