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

201 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Запрос к Опусу — анализ и план развития 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. Ограничения
- **Код не менять.** Только инструкции и план.
- Ответ — в чат, подробно, с обоснованием каждого решения.
- Если есть несколько вариантов — перечислить с плюсами/минусами.