Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
18 KiB
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. Синонимов и категорий (кроме специальности) нет — значит, автосопоставление держится на эмбеддингах и кодах тарификатора, синонимы наращиваем сами.
Выводы, меняющие дизайн:
- Не «резидент/нерезидент», а N ценовых тарифов (резидент, нерезидент, СНГ, дальнее, страховые, партнёр) → гибкая модель цен, а не две колонки.
- Код тарификатора — золотой ключ: где клиника его даёт (Клиника 3), сопоставление точное и бесплатное. Проверяем в первую очередь.
- «Скан» в реальности — это битый текстовый слой (Клиника 3). Детектируем мусор и уходим в Gemini Vision по отрисованной странице. Универсальный Vision-путь закрывает и сканы, и битый текст.
- Файлы большие (десятки страниц, тысячи строк) → нативные парсеры тянут основной объём, Vision бережём для трудных страниц (стоимость и время).
- Шапка не в первой строке, секции-заголовки задают специальность → логика поиска строки заголовков и контекста секции. Специальность из секции сужает поиск по справочнику и повышает точность.
Модель данных
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%)
Каскад по убыванию надёжности:
- Код тарификатора:
service_code_sourceматчитService.tarificator_code→ точное сопоставление,confidence=1.0. - Точное совпадение имени (после нормализации: нижний регистр, схлопывание пробелов/дефисов) с
name_ru. - Эмбеддинги (multilingual sentence-transformers, предрасчёт по 1287
name_ru, pgvector): топ-1 по косинусу; если ≥0.85 → автосопоставление. Если известна специальность секции — поиск сужается внутри специальности (точнее и быстрее). - Нечёткий поиск (RapidFuzz token_set_ratio) как вторичный сигнал/тай-брейк.
- Иначе → очередь
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 клиники сквозь весь конвейер.
- 14–26 ч: B — pdf-парсер + Vision для Клиники 3; C — валидация + версионирование; прогон всего архива; отчёт о качестве. ✅ MVP на реальных данных, % автонормализации измерен.
- 26–34 ч: деплой на домен, наполнение БД, проверка по HTTPS; начало варианта «Шире» (очередь верификации, сравнение цен), если ядро закрыто.
- 34–44 ч: полировка, отчёт, README; заморозка фич; репетиция демо.
- 44–48 ч: буфер, fallback-видео, финальная проверка прода.
Что сдаём (ТЗ §7)
Рабочий MVP + README · обработанная БД по архиву · загруженный справочник · OpenAPI/Swagger · отчёт о качестве (документов, % автонормализации, позиций в очереди) · презентация 5–7 слайдов · демо-видео (опционально).
Контракты, которые замораживаем в Фазе 0
- Схема БД (4 сущности выше, модель цен
prices jsonb+ две денормализованные колонки). openapi.yaml(7 эндпоинтов +/stats).- Контракт парсера (единый формат «сырой строки»).
- Формат загрузки справочника (маппинг колонок xlsx → Service).