# Запрос к Опусу — анализ и план развития 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.py` → `build_classify_prompt(header_text)`. Запрашивает у LLM: ```json {"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. Ограничения - **Код не менять.** Только инструкции и план. - Ответ — в чат, подробно, с обоснованием каждого решения. - Если есть несколько вариантов — перечислить с плюсами/минусами.