Files
medtech-hackathon/docs/PLAN.md
T

147 lines
18 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.
# MedArchive — план реализации (Кейс 2, MedPartners)
**Цель:** собрать систему, которая принимает архив разнородных прайс-листов клиник, извлекает услуги и цены, нормализует их к справочнику из 1287 услуг и отдаёт API и интерфейс для поиска «кто оказывает услугу и по какой цене».
**Архитектура:** Python/FastAPI обрабатывает документы по формату (нативные парсеры + Gemini Vision как универсальный запасной путь), нормализует через коды тарификатора и эмбеддинги, складывает в PostgreSQL и отдаёт REST API. Поверх — Next.js: админка оператора и публичное сравнение цен.
**Стек:** Python 3.12, FastAPI, PostgreSQL + pgvector, pdfplumber, PyMuPDF, openpyxl, xlrd, python-docx, sentence-transformers (multilingual), RapidFuzz, Gemini Vision, Next.js + Tailwind + shadcn/ui, Docker Compose + Caddy.
## Сквозные ограничения
- **Репозиторий публичный** (требование организаторов) → ни одного секрета в коде. Все ключи (Gemini и прочее) — только через переменные окружения и `.env`, который в `.gitignore`. В репо — `.env.example` с пустыми значениями.
- **Качество кода оценивается** → чистая структура модулей, типизация, docstrings и осмысленные комментарии в нетривиальных местах. Линтер (ruff) и форматтер прогоняются перед коммитом.
- **Любой русский текст** (README, комментарии, тексты интерфейса, презентация) проходит проверку `ru-text` с оценкой не ниже 8.5.
- **ИИ-движок:** Gemini Vision для сканов и сложной вёрстки; сопоставление со справочником — локальными эмбеддингами без платных вызовов.
- **Деплой:** живая версия на `med.secondbrain.tools` (Selectel KZ), плюс репозиторий с инструкцией запуска.
- **Целевой показатель ТЗ:** не менее 70% позиций нормализуются автоматически.
---
## Что реально в данных (разведка датасета)
8 клиник, 10 файлов. Реальность заметно сложнее, чем «4 формата + OCR» из ТЗ.
| Клиника | Файл(ы) | Особенности |
|---|---|---|
| 1 | docx 2024 (таблица 2727 строк) + pdf 2026 (85 стр., текст) | Колонки `Код / Наименование / Стоимость, тенге`. Коды `U1.1` (внутренние, не тарификатор). Секции «Раздел 1…». **История цен** 2024→2026. |
| 2 | pdf 2025 + pdf 2026 (текст) | **Резидент / нерезидент / страховые** — несколько колонок цен. Грязные многострочные заголовки. История цен. |
| 3 | PDF 2026 (текст, но **битый текстовый слой** — «Yliliepiia.i») | Колонки `Код тарификатора / Цена / Цена партнёра`. **Коды тарификатора `В02.110.002` совпадают со справочником** → точное сопоставление. Нужен Vision-путь поверх битого текста. |
| 4 | pdf 2026 (текст) | Несколько ценовых категорий по гражданству (РК/оралманы, СНГ, дальнее зарубежье). |
| 5 | pdf 2025 (текст) | Нестандартная вёрстка: первичный/повторный приём, ФИО врача, специальность. Цены «9000 тг». |
| 6 | xlsx (лист 5181×12) | Много пустых ячеек, шапка-преамбула («Приложение 1, к приказу…»), данные начинаются глубоко. |
| 7 | xls (3065×6) | Колонки: №, наименование, ед.изм., **цена РК / СНГ / дальнее зарубежье** (3 тарифа). Шапка не в первой строке. |
| 8 | xlsx (2 листа: 1816×11 и 155×40) | Преамбула-приказ, несколько листов, очень широкие таблицы. |
**Справочник услуг** (`Справочник услуг.xlsx`): 1 лист, **1287 строк × 5 колонок**`ID | Специальность | Code | Name_ru | TarificatrCode`. **Синонимов и категорий (кроме специальности) нет** — значит, автосопоставление держится на эмбеддингах и кодах тарификатора, синонимы наращиваем сами.
**Выводы, меняющие дизайн:**
1. **Не «резидент/нерезидент», а N ценовых тарифов** (резидент, нерезидент, СНГ, дальнее, страховые, партнёр) → гибкая модель цен, а не две колонки.
2. **Код тарификатора — золотой ключ:** где клиника его даёт (Клиника 3), сопоставление точное и бесплатное. Проверяем в первую очередь.
3. **«Скан» в реальности — это битый текстовый слой** (Клиника 3). Детектируем мусор и уходим в Gemini Vision по отрисованной странице. Универсальный Vision-путь закрывает и сканы, и битый текст.
4. **Файлы большие** (десятки страниц, тысячи строк) → нативные парсеры тянут основной объём, Vision бережём для трудных страниц (стоимость и время).
5. **Шапка не в первой строке, секции-заголовки задают специальность** → логика поиска строки заголовков и контекста секции. Специальность из секции сужает поиск по справочнику и повышает точность.
---
## Модель данных
**Service** (справочник, загружается из xlsx): `service_id` (из ID+Code), `specialty`, `name_ru`, `tarificator_code`, `synonyms jsonb` (наращиваем), `embedding vector(384)`, `is_active`.
**Partner:** `partner_id`, `name`, `city`, `address`, `bin`, `contact_email`, `contact_phone`, `is_active`, `created_at`, `updated_at`. Имя/город — из шапки документа или имени файла; дедуп по БИН, иначе по нормализованному имени.
**PriceDocument:** `doc_id`, `partner_id`, `file_name`, `file_format` (pdf/scan_pdf/docx/xlsx/xls), `effective_date` (год из имени файла), `parsed_at`, `parse_status` (pending/processing/done/error/needs_review), `parse_log`, `raw_content`.
**PriceItem:** `item_id`, `doc_id`, `partner_id`, `service_name_raw`, `service_code_source`, `service_id` (nullable), `unit`, **`prices jsonb`** (карта `тариф → сумма`: `resident`, `nonresident`, `cis`, `far`, `insurance`, `partner`…), `price_resident_kzt` и `price_nonresident_kzt` (денормализация двух главных тарифов под ТЗ и API), `currency_original`, `effective_date`, `map_method` (code/exact/embedding/fuzzy/manual), `map_confidence`, `is_verified`, `verification_note`, `is_active`.
Уникальность активной позиции: `(partner_id, service_name_raw, effective_date)`. Версионирование: при новом прайсе старые позиции той же клиники архивируются (`is_active=false`), не удаляются.
---
## Конвейер извлечения (по форматам)
Единый контракт выхода парсера: список «сырых строк» `{service_name_raw, service_code_source, prices{}, unit, section}` + метаданные клиники/даты.
- **XLSX / XLS** (openpyxl / xlrd): обойти все листы; найти строку заголовков (содержит «Наименование» и «Цена»); распознать все колонки цен по заголовкам; строки из одной непустой ячейки = заголовок секции (задаёт специальность); пропустить преамбулу.
- **DOCX** (python-docx): принять все правки (работаем с финальным текстом); вытащить таблицы; обработать секции и большие таблицы.
- **PDF текстовый** (pdfplumber): извлечь таблицы по линиям/координатам; для многоколоночных цен — кластеризация по x-координате; заголовок секции из строк без цены.
- **PDF с битым слоем / скан** (детектор мусора → PyMuPDF рендер → **Gemini Vision** со строгой JSON-схемой): универсальный путь для Клиники 3 и любых сложных страниц.
Детектор формата: расширение + проба текстового слоя (доля кириллицы/словарных слов). Низкое качество → Vision.
---
## Нормализация (ядро 25% оценки, цель ≥70%)
Каскад по убыванию надёжности:
1. **Код тарификатора:** `service_code_source` матчит `Service.tarificator_code` → точное сопоставление, `confidence=1.0`.
2. **Точное совпадение имени** (после нормализации: нижний регистр, схлопывание пробелов/дефисов) с `name_ru`.
3. **Эмбеддинги** (multilingual sentence-transformers, предрасчёт по 1287 `name_ru`, pgvector): топ-1 по косинусу; если ≥0.85 → автосопоставление. Если известна специальность секции — поиск сужается внутри специальности (точнее и быстрее).
4. **Нечёткий поиск** (RapidFuzz token_set_ratio) как вторичный сигнал/тай-брейк.
5. Иначе → очередь `unmatched` на ручную разметку.
Порог конфигурируется. Подтверждённые операторы-сопоставления пополняют `synonyms` — система учится по ходу.
---
## Валидация (20% оценки, правила ТЗ §4.4)
Движок проверок при парсинге: цена > 0 и число; нерезидент ≥ резидент; имя услуги не пустое; дата не в будущем; дубликат `(клиника, услуга, дата)` → архивировать старую; изменение цены > 50% к прошлой версии → флаг аномалии; валюта ≠ KZT → конвертация по курсу на дату, оригинал сохраняем; документ без распознаваемых данных → статус `error`. Нарушения пишутся в `parse_log` и/или ставят `needs_review`.
---
## API (15%) + WOW-слой
REST (FastAPI, OpenAPI обязателен):
`GET /services` (фильтр по специальности) · `GET /services/{id}/partners` (кто оказывает + цены) · `GET /partners` (фильтр город/статус) · `GET /partners/{id}/services` · `GET /search?q=` (полнотекст по услугам и клиникам) · `GET /unmatched` · `POST /match`. Плюс `GET /stats` для дашборда и отчёта о качестве.
**WOW-слой (тонкий, из ТЗ MedPrice):** публичная страница сравнения — поиск услуги → таблица клиник с ценами резидент/нерезидент, сортировка, подсветка самой выгодной. Это «Aviasales для медицины» поверх наших данных — показывает заказчику готовый кусок второго продукта.
---
## Роли агентов (worktrees) и владение
| Агент | Владеет | Ответственность |
|---|---|---|
| **A — Контракты и данные** | `contracts/`, `db/migrations/`, `fixtures/` | Схема БД, Pydantic/Zod-модели, `openapi.yaml`, загрузчик справочника (1287 услуг + предрасчёт эмбеддингов) |
| **B — Извлечение** | `etl/extractors/` | Парсеры xlsx/xls/docx/pdf + Vision-путь + детектор формата/мусора + поиск заголовков |
| **C — Нормализация и валидация** | `etl/normalize/`, `etl/validate/` | Каскад сопоставления, очередь unmatched, движок проверок, версионирование |
| **D — API** | `api/` | Эндпоинты, поиск, OpenAPI, `/stats` |
| **E — Фронт** | `web/` | Админка (загрузка, статусы, очередь верификации, дашборд) + публичное сравнение цен |
| **F — Инфра/деплой/доки** | `infra/`, `docs/`, `pitch/` | Docker Compose, Caddy, деплой, README, отчёт о качестве, презентация |
Человек-оркестратор: держит контракты, мёрджит в `main`, ведёт борд статусов.
---
## MVP cut-line
- **MUST (ядро 75% оценки):** загрузка архива → детект формата → парсеры 4 форматов + Vision-fallback → извлечение PriceItem с N-тарифами → нормализация (код→точное→эмбеддинги→unmatched) → схема Postgres → движок валидации → API (7 эндпоинтов) → **отчёт о качестве** (обработано, % автонормализации, очередь).
- **SHOULD (воскресенье, вариант «Шире»):** UI очереди верификации + `POST /match`, версионирование цен, конвертация валют, детектор аномалий, публичное сравнение цен (WOW).
- **NICE:** дашборд-метрики, полировка дизайна, демо-видео, дедуп по БИН, наращивание синонимов.
---
## Тайм-план (T0 = старт сборки, датасет на руках)
- **T0–4 ч — Фаза 0:** скелет монорепо + worktrees + per-role `CLAUDE.md`; A замораживает схему БД + `openapi.yaml` + загрузчик справочника; E поднимает Compose. ✅ контракты заморожены.
- **4–14 ч:** B — парсеры xlsx/docx (самые чистые форматы) на реальных файлах; C — каскад нормализации на справочнике; D — API на сид-данных; E — каркас админки. ✅ smoke: 2 клиники сквозь весь конвейер.
- **1426 ч:** B — pdf-парсер + Vision для Клиники 3; C — валидация + версионирование; прогон всего архива; отчёт о качестве. ✅ MVP на реальных данных, % автонормализации измерен.
- **26–34 ч:** деплой на домен, наполнение БД, проверка по HTTPS; начало варианта «Шире» (очередь верификации, сравнение цен), если ядро закрыто.
- **34–44 ч:** полировка, отчёт, README; **заморозка фич**; репетиция демо.
- **4448 ч:** буфер, fallback-видео, финальная проверка прода.
---
## Что сдаём (ТЗ §7)
Рабочий MVP + README · обработанная БД по архиву · загруженный справочник · OpenAPI/Swagger · **отчёт о качестве** (документов, % автонормализации, позиций в очереди) · презентация 5–7 слайдов · демо-видео (опционально).
---
## Контракты, которые замораживаем в Фазе 0
1. **Схема БД** (4 сущности выше, модель цен `prices jsonb` + две денормализованные колонки).
2. **`openapi.yaml`** (7 эндпоинтов + `/stats`).
3. **Контракт парсера** (единый формат «сырой строки»).
4. **Формат загрузки справочника** (маппинг колонок xlsx → Service).