Files
medtech-hackathon/docs/PLAN.md
T

18 KiB
Raw Blame History

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 = старт сборки, датасет на руках)

  • T04 ч — Фаза 0: скелет монорепо + worktrees + per-role CLAUDE.md; A замораживает схему БД + openapi.yaml + загрузчик справочника; E поднимает Compose. контракты заморожены.
  • 414 ч: B — парсеры xlsx/docx (самые чистые форматы) на реальных файлах; C — каскад нормализации на справочнике; D — API на сид-данных; E — каркас админки. smoke: 2 клиники сквозь весь конвейер.
  • 1426 ч: B — pdf-парсер + Vision для Клиники 3; C — валидация + версионирование; прогон всего архива; отчёт о качестве. MVP на реальных данных, % автонормализации измерен.
  • 2634 ч: деплой на домен, наполнение БД, проверка по HTTPS; начало варианта «Шире» (очередь верификации, сравнение цен), если ядро закрыто.
  • 3444 ч: полировка, отчёт, README; заморозка фич; репетиция демо.
  • 4448 ч: буфер, fallback-видео, финальная проверка прода.

Что сдаём (ТЗ §7)

Рабочий MVP + README · обработанная БД по архиву · загруженный справочник · OpenAPI/Swagger · отчёт о качестве (документов, % автонормализации, позиций в очереди) · презентация 5–7 слайдов · демо-видео (опционально).


Контракты, которые замораживаем в Фазе 0

  1. Схема БД (4 сущности выше, модель цен prices jsonb + две денормализованные колонки).
  2. openapi.yaml (7 эндпоинтов + /stats).
  3. Контракт парсера (единый формат «сырой строки»).
  4. Формат загрузки справочника (маппинг колонок xlsx → Service).