14 KiB
Как работает сервис «Сверка договоров»
Документ описывает текущую программную реализацию contracts-flask: последовательность обработки данных, ответственность модулей и ключевые функции. Основной актуальный код находится в site/; каталог deploy/ является историческим VM-слоем.
1. Общий поток
Браузер
-> выбор файлов / ZIP
-> загрузка через VM-буфер
-> парсинг
-> классификация документов
-> группировка по договорам
-> применение подтверждённых групп
-> SSE-сравнение документов по порядку
-> event sourcing: spec_events + spec_current
-> таблица изменений и чат по текущей спецификации
Главная точка сборки Flask-приложения: site/app.py. Регистрация маршрутов выполняется через site/routes/__init__.py.
2. Загрузка файлов
Фронтенд начинает обработку в site/static/files.js:
onFilesSelected()принимает выбранные файлы, обрабатывает ZIP и обычные документы, запускает подготовку загрузки и сохраняет связьzip_source.uploadFile()запускает загрузку через VM-буфер; фактические PUT и отправка ссылок выполняются функциямиuploadViaVM()иputToVm()вupload/frontend/upload/upload_via_vm.jsиupload/frontend/upload/put_to_vm.js.finalizeUpload()завершает загрузку и обновляет состояние.renderFiles(state)отображает список файлов, группируя вложения поzip_source.syncDB()синхронизирует состав файлов в БД.
Из-за ограничения managed-шлюза большие файлы сначала передаются на ВМ, а затем бэкенд забирает их исходящим запросом. Общий переиспользуемый транспорт находится в upload/backend/upload_refs/blueprint.py, включая обработчик POST /api/upload_refs; конкретная бизнес-логика передаётся через sink.
Маршруты загрузки основного приложения находятся в site/routes/upload_bp.py. Там принимаются ссылки/файлы, вызывается sink сервиса договоров и сохраняются результаты обработки.
3. Парсинг
Парсер находится в site/services/parse.py. Он преобразует содержимое документов в структурированный список элементов:
parse()выбирает обработчик по формату.- Обработчик DOCX использует
python-docxи извлекает параграфы и таблицы. - Обработчик PDF использует
pdfplumber. - TXT обрабатывается напрямую.
- Для
.docфункцияparse_file()в текущемsiteвызывает_parse_docx(data); отдельныйconvert-service/app.pyи историческийdeploy/convert_doc.pyне являются частью этого вызова.
Результат парсинга сохраняется как elements_json. Важный принцип: парсер не решает, какие строки являются услугами, а сохраняет исходную структуру документа для следующих стадий.
4. Классификация документов
Маршрут запуска классификации: site/routes/pipeline_bp.py, функция classify_batch_route().
- Для небольшого батча классификация выполняется синхронно.
- Для большого батча запускается фоновый поток и возвращается
202. process_v2()отдаёт поток SSE для этапа сравнения.
Основная логика находится в 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. Результат содержит doc_type, own_number, parent_number, doc_date и counterparty. Сырые вход и ответ LLM также сохраняются для диагностики.
5. Группировка по договорам
Логика находится в 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/static/groups.js.
Таким образом, LLM извлекает признаки документа, но окончательное сопоставление по номерам выполняется обычным Python-кодом.
6. Последовательное сравнение
Маршрут сравнения: process_v2() в site/routes/pipeline_bp.py. Он проверяет contract_id, запускает run_pipeline() и преобразует события в формат SSE.
Основной pipeline находится в 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получает текущие строки и текст документа и возвращает операции.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проверяет согласованностьsum,priceиqty.
Промпты извлечения и сравнения формируются через build_prompt() в site/llm_prompt.py; версии промптов хранятся и редактируются маршрутами из site/routes/prompts_bp.py.
7. Event sourcing и текущее состояние
Механизм хранения находится в 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/connection.py.
8. SSE и интерфейс
Клиент сравнения находится в site/static/compare.js:
startCompareSSE()открываетEventSource, принимает события и управляет таймером.applyCompareEvent()переводит SSE-события в состояние секций.renderCompareSectionHeader()формирует заголовок секции документа.renderCompareSectionBody()отображает итоговую статистику и операции.renderCompareOpsTable()строит таблицу изменений.
Оркестрация шагов загрузки, классификации, группировки и запуска сравнения находится в site/static/app.js, в частности в runClassify() и обработчиках loadGroupsAction()/сравнения.
9. Чат
Маршрут чата находится в site/routes/api_bp.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, нормализация номеров, порядок, проверки и сохранение результата выполняются детерминированным кодом.