From 83002b9644d3739f954cad894c9556bccdccf8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 31 Aug 2026 13:58:15 +0300 Subject: [PATCH] 1 --- docs/service-how-it-works.md | 145 +++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 docs/service-how-it-works.md diff --git a/docs/service-how-it-works.md b/docs/service-how-it-works.md new file mode 100644 index 0000000..f826a94 --- /dev/null +++ b/docs/service-how-it-works.md @@ -0,0 +1,145 @@ +# Как работает сервис «Сверка договоров» + +Документ описывает текущую программную реализацию `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. Загрузка файлов + +Фронтенд начинает обработку в [`site/static/files.js`](../site/static/files.js): + +- `onFilesSelected()` принимает выбранные файлы, обрабатывает ZIP и обычные документы, запускает подготовку загрузки и сохраняет связь `zip_source`. +- `uploadFile()` запускает загрузку через VM-буфер; фактические PUT и отправка ссылок выполняются функциями `uploadViaVM()` и `putToVm()` в [`upload/frontend/upload/upload_via_vm.js`](../upload/frontend/upload/upload_via_vm.js) и [`upload/frontend/upload/put_to_vm.js`](../upload/frontend/upload/put_to_vm.js). +- `finalizeUpload()` завершает загрузку и обновляет состояние. +- `renderFiles(state)` отображает список файлов, группируя вложения по `zip_source`. +- `syncDB()` синхронизирует состав файлов в БД. + +Из-за ограничения 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, нормализация номеров, порядок, проверки и сохранение результата выполняются детерминированным кодом.