Files
contracts-flask/docs/service-how-it-works.md
T
RepinoidandCopilot 6e6fc4db8e
Deploy contracts-flask / validate (push) Canceled after 0s
Integrate Obfuscator file picker into Contracts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-30 10:24:23 +03:00

148 lines
15 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-flask`: последовательность обработки данных, ответственность модулей и ключевые функции. Основной актуальный код находится в `site/`; каталог `deploy/` является историческим VM-слоем.
## 1. Общий поток
```text
Браузер
-> выбор файлов / ZIP
-> загрузка через VM-буфер
-> парсинг
-> классификация документов
-> группировка по договорам
-> применение подтверждённых групп
-> SSE-сравнение документов по порядку
-> event sourcing: spec_events + spec_current
-> таблица изменений и чат по текущей спецификации
```
Главная точка сборки Flask-приложения: [`site/app.py`](../site/app.py). Регистрация маршрутов выполняется через [`site/routes/__init__.py`](../site/routes/__init__.py).
## 2. Загрузка файлов
Выбор файлов реализован в [`upload/frontend/index.js`](../upload/frontend/index.js) и модулях `table/` и `zip/`; собранный IIFE подключается на странице через `/file-picker/file-picker.iife.js`. Picker строит дерево, раскрывает ZIP, а при выборе папки применяет корневой `obscure.lst`.
Связка с приложением находится в [`site/static/app.js`](../site/static/app.js) и [`site/static/files.js`](../site/static/files.js):
- `onPickerFilesChange()` синхронизирует leaf-файлы дерева с `state.files`, вычисляет `zip_source` как путь до последнего ZIP в цепочке и ставит новые файлы в последовательную очередь.
- `uploadPickerEntry()` вызывает `FilePicker.uploadViaVM()` для одного файла: PUT в VM-буфер, затем POST `/api/upload_refs` с `batch_id`, `contract_id` и `zip_source`.
- Результат sink (`doc_id`, `parsed` или duplicate/error) отображается в строке дерева; детали и распарсенный текст доступны из действий строки.
- `finalizeUpload()` ожидает завершения очереди и синхронизации, затем включает классификацию.
- `syncDB()` синхронизирует только текущий `batch_id`; перед удалением документа ждёт активную очередь загрузки.
Из-за ограничения managed-шлюза большие файлы сначала передаются на ВМ, а затем бэкенд забирает их исходящим запросом. Общий переиспользуемый транспорт находится в [`upload/backend/upload_refs/blueprint.py`](../upload/backend/upload_refs/blueprint.py), включая обработчик `POST /api/upload_refs`; конкретная бизнес-логика передаётся через `sink`.
Маршруты загрузки основного приложения находятся в [`site/routes/upload_bp.py`](../site/routes/upload_bp.py). Там принимаются ссылки/файлы, вызывается sink сервиса договоров и сохраняются результаты обработки.
## 3. Парсинг
Парсер находится в [`site/services/parse.py`](../site/services/parse.py). Он преобразует содержимое документов в структурированный список элементов:
- `parse()` выбирает обработчик по формату.
- Обработчик DOCX использует `python-docx` и извлекает параграфы и таблицы.
- Обработчик PDF использует `pdfplumber`.
- TXT обрабатывается напрямую.
- Для `.doc` функция `parse_file()` в текущем `site` вызывает `_parse_docx(data)`; отдельный [`convert-service/app.py`](../convert-service/app.py) и исторический [`deploy/convert_doc.py`](../deploy/convert_doc.py) не являются частью этого вызова.
Результат парсинга сохраняется как `elements_json`. Важный принцип: парсер не решает, какие строки являются услугами, а сохраняет исходную структуру документа для следующих стадий.
## 4. Классификация документов
Маршрут запуска классификации: [`site/routes/pipeline_bp.py`](../site/routes/pipeline_bp.py), функция `classify_batch_route()`.
- Для небольшого батча классификация выполняется синхронно.
- Для большого батча запускается фоновый поток и возвращается `202`.
- `process_v2()` отдаёт поток SSE для этапа сравнения.
Основная логика находится в [`site/services/classify.py`](../site/services/classify.py):
- `classify_batch(batch_id, llm_client=None, repo=None)` получает pending-документы, запускает параллельную обработку через `ThreadPoolExecutor` и сохраняет результат.
- `_classify_one()` выполняет полный цикл для одного файла.
- `_is_garbage_by_filename()` отбрасывает очевидный мусор по имени файла.
- `_smart_extract()` формирует компактную выжимку: начало документа и найденные фрагменты с ключевыми маркерами.
- `_is_garbage_by_header()` отбрасывает документы по заголовочным маркерам.
- `_call_llm_classify()` отправляет выжимку в LLM.
- `_safe_json_parse()` извлекает JSON из ответа LLM и исправляет распространённые ошибки форматирования.
Промпт формируется функцией `build_classify_prompt()` в [`site/llm_prompt.py`](../site/llm_prompt.py). Результат содержит `doc_type`, `own_number`, `parent_number`, `doc_date` и `counterparty`. Сырые вход и ответ LLM также сохраняются для диагностики.
## 5. Группировка по договорам
Логика находится в [`site/services/grouping.py`](../site/services/grouping.py):
- `normalize_number(num)` приводит номер к верхнему регистру и убирает разделители.
- `group_documents(batch_id)` отделяет договоры от допников/спецификаций, сопоставляет их по `parent_number` или `own_number`, создаёт виртуальные группы при отсутствии базового договора и помещает нерешённые документы в `__unresolved__`.
- Внутри групп документы сортируются по `doc_date`.
- `apply_groups(batch_id, groups_data)` сохраняет подтверждённые группы в таблицы договоров и допников.
Маршруты чтения и применения групп находятся в [`site/routes/api_bp.py`](../site/routes/api_bp.py). Фронтенд отображает результат через [`site/static/groups.js`](../site/static/groups.js).
Таким образом, LLM извлекает признаки документа, но окончательное сопоставление по номерам выполняется обычным Python-кодом.
## 6. Последовательное сравнение
Маршрут сравнения: `process_v2()` в [`site/routes/pipeline_bp.py`](../site/routes/pipeline_bp.py). Он проверяет `contract_id`, запускает `run_pipeline()` и преобразует события в формат SSE.
Основной pipeline находится в [`site/services/process.py`](../site/services/process.py):
- `run_pipeline(contract_id, order_ids, build_prompt_fn)` сбрасывает предыдущее состояние, получает документы группы и определяет порядок обработки.
- `_elements_to_text(ej)` преобразует `elements_json` в текстовый контекст для LLM.
- Для каждого документа читается текущая спецификация.
- `call_llm()` из [`site/services/llm.py`](../site/services/llm.py) получает текущие строки и текст документа и возвращает операции.
- `target_id` вида `r1`, `r2` переводится в `target_hash` соответствующей строки текущей спецификации.
- Для режима `full_replace` вызывается `clear_current()`, чтобы новая редакция не дублировала старые строки.
- Затем вызывается `apply_ops()` и отправляются SSE-события `extract_start`, `llm_done`, `applied`, `extract_error` и финальное `complete`.
- `check_arithmetic()` из [`site/services/metrics.py`](../site/services/metrics.py) проверяет согласованность `sum`, `price` и `qty`.
Промпты извлечения и сравнения формируются через `build_prompt()` в [`site/llm_prompt.py`](../site/llm_prompt.py); версии промптов хранятся и редактируются маршрутами из [`site/routes/prompts_bp.py`](../site/routes/prompts_bp.py).
## 7. Event sourcing и текущее состояние
Механизм хранения находится в [`site/db/spec_events.py`](../site/db/spec_events.py):
- `reset(contract_id)` очищает события и текущее состояние перед новым полным прогоном.
- `clear_current(contract_id)` очищает только текущую спецификацию для `full_replace`.
- `apply_ops(...)` обрабатывает `ADD`, `UPDATE`, `DELETE` и `UNRESOLVED`.
- `_hash(name, date_start)` создаёт стабильный идентификатор строки услуги.
- `_upsert_spec_current(...)` добавляет или обновляет строку текущей спецификации.
- `_update_spec_current(...)` изменяет только поля, указанные в операции.
- `_log_unresolved(...)` сохраняет нерешённую операцию вместо молчаливого пропуска.
`spec_events` — журнал операций с исходным ответом LLM, версией промпта и ссылкой на документ. `spec_current` — материализованное состояние, используемое для следующего сравнения и отображения.
CRUD текущей спецификации и исходных данных документа находится в [`site/db/spec_current.py`](../site/db/spec_current.py). Схема и соединения описаны в [`site/db/connection.py`](../site/db/connection.py).
## 8. SSE и интерфейс
Клиент сравнения находится в [`site/static/compare.js`](../site/static/compare.js):
- `startCompareSSE()` открывает `EventSource`, принимает события и управляет таймером.
- `applyCompareEvent()` переводит SSE-события в состояние секций.
- `renderCompareSectionHeader()` формирует заголовок секции документа.
- `renderCompareSectionBody()` отображает итоговую статистику и операции.
- `renderCompareOpsTable()` строит таблицу изменений.
Оркестрация шагов загрузки, классификации, группировки и запуска сравнения находится в [`site/static/app.js`](../site/static/app.js), в частности в `runClassify()` и обработчиках `loadGroupsAction()`/сравнения.
## 9. Чат
Маршрут чата находится в [`site/routes/api_bp.py`](../site/routes/api_bp.py). Он получает вопрос пользователя, загружает текущую спецификацию через функции из [`site/db/spec_current.py`](../site/db/spec_current.py), формирует контекст и отправляет его в LLM. Поэтому чат отвечает по материализованному состоянию договора, а не по случайному отдельному документу.
## 10. Итоговая ответственность компонентов
| Слой | Ответственность |
|---|---|
| `site/static/*.js` | выбор файлов, загрузка, отображение прогресса и результатов |
| `site/routes/` | HTTP API, SSE и связывание компонентов |
| `site/services/parse.py` | извлечение структуры документа |
| `site/services/classify.py` | классификация и выделение метаданных через LLM |
| `site/services/grouping.py` | детерминированное сопоставление документов |
| `site/services/process.py` | последовательное сравнение группы |
| `site/services/llm.py` | вызов LLM для анализа |
| `site/db/spec_events.py` | применение операций и аудит изменений |
| `site/db/spec_current.py` | чтение текущей спецификации и элементов документов |
| `site/db/connection.py` | SQLite/WAL и доступ к данным |
Итог: LLM используется там, где требуется понять содержание документа и смысл изменения услуги. Структура pipeline, нормализация номеров, порядок, проверки и сохранение результата выполняются детерминированным кодом.