201 lines
11 KiB
Markdown
201 lines
11 KiB
Markdown
# Запрос к Опусу — анализ и план развития 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. Ограничения
|
||
|
||
- **Код не менять.** Только инструкции и план.
|
||
- Ответ — в чат, подробно, с обоснованием каждого решения.
|
||
- Если есть несколько вариантов — перечислить с плюсами/минусами.
|