docs: план реализации MedArchive, заземлённый на реальный датасет

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 14:59:04 +05:00
parent 39b45567a1
commit f35957a31e
+146
View File
@@ -0,0 +1,146 @@
# 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).