Files
contracts/Files/opus-request-zip-plan.md

11 KiB
Raw Permalink Blame History

Запрос к Опусу — анализ и план развития Contracts App (v1.0.178)

Дата: 26.06.2025. Только инструкции, код не менять.


1. Контекст: что за сервис

Сервис «Сверка договоров» — обработка договоров облачного провайдера НУБЕС с контрагентами. Пользователь загружает ZIP-архивы с документами (договоры, допсоглашения, спецификации), система классифицирует, группирует по номерам договоров и сравнивает спецификации услуг — показывает diff (ADD/UPDATE/DELETE/UNRESOLVED).

Архитектура:

  • Lucee (CFML) — фронтенд, домен contractor.luceek8s.dev.nubes.ru
  • Python (VM) — бэкенд, домен contracts.kube5s.ru, порт 8766
  • PostgreSQL — хранение
  • nginx — прокси
  • LLM — api.aillm.ru, модель gpt-oss-120b, HTTP/2 через httpx

Пайплайн:

  1. Upload (.docx/.pdf/.doc/.zip) → парсинг (python-docx/pdfplumber) → elements_json
  2. Classify — LLM определяет: тип (contract/supplement/specification/other), own_number, parent_number, counterparty, doc_date
  3. Group — Python normalize_number() + matching по номеру договора
  4. Compare — SSE, LLM сравнивает спецификации: ADD/UPDATE/DELETE/UNRESOLVED

2. Текущий код — ключевые файлы

2.1. Classify

Файл: contractor/deploy/services/classify.py

  • MAX_WORKERS = 4 — параллельные запросы к LLM
  • _smart_extract() — выжимка текста: заголовок ~1500 симв + regex-хиты по маркерам
  • _call_llm_classify() — вызов LLM, парсинг JSON из ответа
  • classify_batch() — ThreadPoolExecutor, классифицирует все pending документы
  • _safe_json_parse() — чинит битый JSON из LLM (markdown, trailing commas)

Промпт classify: llm_prompt.pybuild_classify_prompt(header_text). Запрашивает у LLM:

{"doc_type": "contract|supplement|specification|other",
 "own_number": "XXX001-03700",
 "parent_number": "XXX001-03700",       // для допников/спек
 "doc_date": "2025-01-01",
 "counterparty": "ООО \"Ромашка\""}

Проблема classify: при 50+ файлах синхронный вызов таймаутился. Сейчас: subprocess.Popen → classify_worker.py (async, 202 Accepted). Но фронтенд (app.js runClassify()) не полностью синхронизирован с async-ответом.

2.2. Group

Файл: contractor/deploy/services/grouping.py

  • normalize_number() — uppercase + только буквы/цифры. «МЭС-123/2024» == «МЭС 123/2024» после нормализации.
  • group_documents() — иерархический алгоритм:
    1. contract → якорь группы
    2. supplement/spec → matching по parent_number (нормализованный)
    3. Оставшиеся → виртуальные группы по own_number
    4. Без номеров → __unresolved__
  • apply_groups() — создаёт записи в DB (contracts + supplements)

Известные баги normalize_number:

  • Кириллическая О vs цифра 0 — не различаются
  • Латинская C vs кириллическая С — не различаются
  • Ё → выпадает

2.3. Compare

Файлы: contractor/deploy/convert_server.py (SSE endpoint /process-v2), contractor/deploy/compare.js (фронтенд SSE-клиент).

Промпты compare: llm_prompt.py

  • FALLBACK_EXTRACT — для первого документа (базовый договор/спецификация): ADD всех строк.
  • FALLBACK_DIFF — для допсоглашений: UPDATE/DELETE/ADD/UNRESOLVED. Поддерживает два режима:
    • "mode": "partial" — точечные изменения
    • "mode": "full_replace" — полная замена спецификации

Проблема compare: оба режима — «ADD всех строк». LLM не всегда корректно сопоставляет строки между v1 и v2 спецификации.

2.4. ZIP handling

Файлы: contractor/deploy/files.js (addZipFile()), contractor/deploy/convert_server.py (_handle_unzip_upload()).

Текущая логика:

  1. ZIP загружается через FormData на /unzip-upload
  2. Бэкенд распаковывает, возвращает base64 каждого файла
  3. Фронтенд для каждого: base64 → Blob → File → upload (как addRegularFile)
  4. Дубликаты по имени: confirm-диалог, перезапись

Чего нет:

  • zip_source — связь файла с родительским ZIP
  • Группировка в таблице по ZIP-источнику
  • ID файла = zip_source + "/" + filename

2.5. Фронтенд

Ключевые JS-модули: contractor/deploy/ — state.js, files.js, groups.js, compare.js, app.js, app_utils.js

Таблица файлов: files.js renderFiles() — плоский список, без группировки по ZIP. Уже есть max-height: 50vh; overflow-y: auto (скролл).

Степпер: app.js renderStepper() — 4 шага: Загрузка → Классификация → Группировка → Сравнение.


3. Что сказал заказчик (26.06.2025)

«А что касается вводных — файлы могут быть в зипах. Как правило, по 1 контрагенту, но я считаю, что по контрагенту может быть несколько зипов, и теоретически могу представить ситуацию, когда будет в одном зипе по нескольким как-то связанным контрагентам (какие-то агентские схемы). Наверно, вероятность того, что в одном зипе один контрагент — весьма высокая, но не 100%.»

Также обсуждалось:

  • Привязка файлов к родительскому ZIP
  • В таблице — ZIP как группирующий заголовок, ниже со сдвигом — вложенные файлы
  • ID файла = имя зипа + "/" + имя файла

4. Что обсуждали мы (ключевые выводы)

  1. НУБЕС — всегда Исполнитель. Контрагенты — Заказчики.
  2. Один batch = один контрагент (почти всегда). Хоть 1 зип, хоть 5 зипов — всё по одному контрагенту.
  3. Агентские схемы — исключение, не основная логика.
  4. zip_source — для визуальной группировки в таблице. Не влияет на логику classify/group/compare.
  5. Два сценария UI:
    • «Точный» (≤100 файлов) — текущий UI с таблицей и ручным контролем
    • «Поток» (100+) — упрощённый UI: прогресс-бар, авто-пайплайн, сводный отчёт (без таблицы — 1000 DOM-строк вешают браузер)
  6. Коллизия одинаковых имён из разных ZIP:
    • ID = zip_source + "/" + filename → разные сущности
    • Compare покажет diff между версиями

5. Что нужно от Опуса

5.1. ПЛАН по zip_source

Как именно внедрить привязку файлов к родительскому ZIP на ВСЕХ слоях:

  • unzip.py → возвращать zip_source
  • db/documents.py → поле zip_source
  • files.js renderFiles() → группировка + отступ
  • state.js → поле zip_source
  • index.cfm / Lucee → надо ли менять?

Варианты отображения:

  • Вариант А: ZIP как секция-заголовок, под ним файлы с отступом
  • Вариант Б: древовидная структура (раскрывающиеся ZIP'ы)
  • Вариант В: цветовая маркировка по ZIP

Какой лучше для пользователя и почему?

5.2. ПЛАН по двум сценариям UI

Как архитектурно разделить «Точный» и «Поток» режимы:

  • Общий код (classify/group/compare не меняются)
  • Разный UI: что показывать, что скрывать
  • Как переключаться между режимами (авто по кол-ву файлов? ручной выбор?)
  • «Поток» — сводный отчёт: структура, что в нём

5.3. АНАЛИЗ текущих промптов

Внимательно изучи llm_prompt.py:

  • Есть ли проблемы в классификации (counterparty не определяется)
  • Достаточно ли хорош diff-промпт (compare)
  • Нужны ли разные варианты промптов для разных сценариев:
    • Только НУБЕС-один контрагент (стандартный)
    • Два контрагента (агентские схемы)
    • Мусорные документы (акты сверки, счета)
  • Нужна ли корректировка глоссария, примеров

5.4. РЕКОМЕНДАЦИИ по улучшению

Что ещё можно улучшить, исходя из анализа кода и требований заказчика:

  • Приоритеты: что делать в первую очередь
  • Риски: что может сломаться
  • Оценка трудозатрат (грубо: маленькая/средняя/большая задача)

6. Ограничения

  • Код не менять. Только инструкции и план.
  • Ответ — в чат, подробно, с обоснованием каждого решения.
  • Если есть несколько вариантов — перечислить с плюсами/минусами.