Files
contracts-flask/docs/service-how-it-works.md
“Naeel” 83002b9644
Deploy contracts-flask / validate (push) Canceled after 0s
1
2026-08-31 13:58:15 +03:00

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, нормализация номеров, порядок, проверки и сохранение результата выполняются детерминированным кодом.