🏗️ Архитектура — сверка договоров

Пайплайн: загрузка → парсинг → классификация → группировка → сравнение → аудит


1. Загрузка файлов

Пользователь выбирает .docx / .pdf / .doc / .zip.

Результат: elements_json — массив элементов двух типов:

Документ сохраняется в БД, считается content_hash (SHA-256) — если в этом же батче уже есть файл с таким содержимым, он помечается как duplicate и не дублируется.

📋 Заказчик: «100+ файлов за раз. Файлы: .docx/.pdf, возможно ZIP.
Загрузка из локали (файловая шара / облачный диск), НЕ S3 (конфиденциальность).»

2. Классификация документов

Каждый загруженный файл проходит трёхэтапный фильтр.

📋 Заказчик: «Оставлять только: договоры, доп. соглашения, спецификации.
Игнорировать: акты сверки, счета/счета-фактуры/УПД, акты услуг, платёжки.»

Этап 1 — имя файла 0 токенов

Проверка имени файла на мусорные ключевые слова: «счёт», «акт», «платёж», «УПД», «сверка», «invoice» и др. Совпадение → garbage, дальше не идёт.

Этап 2 — заголовки в тексте 0 токенов

Проверка первых 2000 символов текста на заголовки: «СЧЕТ-ФАКТУРА», «АКТ СВЕРКИ», «АКТ ОКАЗАННЫХ УСЛУГ» и т.п. Совпадение → garbage.

📋 Заказчик: «Названия документов неинформативные. Есть лишние документы.
По всем контрагентам.»

Этап 3 — LLM LLM

Оставшиеся документы отправляются в LLM. Модель получает выжимку текста (первые 1500 символов + строки с маркерами «договор», «соглашение», «спецификация») и возвращает JSON:

📋 Заказчик: «НУБЕС = Исполнитель во всех целевых договорах.»
→ counterparty = другая сторона (Заказчик).

3. Группировка по договорам

Детерминированная (без LLM). Документы группируются по номерам договоров.

📋 Заказчик: «Система сама должна разбираться, кто к кому.
зачем нам базовый договор? Вся информация есть в допниках и спеках.»
«Я не должен распознавать ничего. Система должна сделать всё.»

4. Сравнение — извлечение + дифф

Для каждой группы запускается последовательная обработка.

📋 Заказчик: «Важно. В произвольной, не повторяющейся.» (порядок допников)
«Главное — точность разбора, не UI. Сначала базовая функция → потом юзабельность.»

4a. Сортировка

Документы внутри группы сортируются по doc_date (из классификации), затем по времени загрузки.

4b. Базовый договор (первый в группе) LLM

4c. Дополнительные соглашения LLM

4d. Режим «изложить в новой редакции»

Если допник говорит «изложить в новой редакции» — LLM возвращает полный новый список (full_replace). Все старые строки заменяются новыми.


5. Защита от дурака и злонамеренности защита

Угроза Защита
ZIP-бомба (архив 1 KB → 10 GB) Лимит 500 файлов, 500 MB, ratio сжатия ≤ 100×
Path traversal в ZIP (../../etc/passwd) Проверка имени файла ДО os.path.basename
Битые кодировки имён в ZIP cp437 → utf8 перекодировка
Файлы не-Word/PDF (exe, картинки, etc.) Whitelist: только pdf/docx/doc/zip — остальные unknown_format
Дубликаты файлов (тот же контент, другое имя) sha256(original_bytes) → пропуск с пометкой duplicate
Дубликаты услуг (одна услуга в разных допниках) UPSERT по name_hash (имя + дата начала) — разные периоды = разные строки
ADD без названия услуги UNRESOLVED, не применяется
UPDATE/DELETE без идентификатора цели Пустой target_hashUNRESOLVED
Неизвестный тип операции от LLM UNRESOLVED
Кривые числа (1 000,50) Авто-нормализация: пробелы → удалить, запятая → точка
Кривые даты (01.03.2026) Авто-нормализация: DD.MM.YYYY → YYYY-MM-DD
Арифметические ошибки Проверка price × qty = sum — расхождение → флаг
Мусорные документы (счета, акты, платёжки) Трёхэтапный фильтр: имя файла → заголовки (2000 симв.) → LLM
Тихие ошибки (try/except: pass) Везде logging.warning с контекстом
📋 Заказчик: «кривых документов можно ожидать.
Если не может разобраться — пусть зовет на помощь юзера.»

6. Event Sourcing — аудит аудит

📋 Заказчик: «Наверно я захочу иметь возможность посмотреть любые промежуточные результаты.
Было бы хорошо пояснить или показать процесс, что в каком порядке происходит.»

7. Промпты LLM

Три роли промптов, хранятся в БД с версионированием:

Роль Назначение
extract Извлечение строк спецификации из договора
diff Сравнение допников с текущей спецификацией
classify Определение типа/номера/даты/контрагента документа

Встроенный редактор промптов с полным CRUD и историей версий:


8. Хранение документов конфиденциальность

📋 Заказчик: «Загрузка из локали, НЕ S3 (конфиденциальность).»

Исходные бинарные файлы (.docx/.pdf/.doc/zip) удаляются с сервера сразу после парсинга. На диске не остаются.

В БД сохраняются — это и есть рабочие данные системы:

⚠️ Пока не реализовано: удаление промежуточных данных после сессии. Варианты: кнопка «Очистить» в интерфейсе, либо Redis с TTL. Надо решать.


9. Инженерные практики качество кода

Decoupling — разделение ответственности

Пайплайн разбит на независимые слои с чёткими интерфейсами:

Слой Что делает Можно заменить отдельно
Контракты данных 5 frozen dataclass'ов: ParseResult, ClassifyResult, GroupingResult, BatchGroupingResult, CompareOp Формат данных не привязан к хранилищу
LLM-клиент Протокол LLMClient.complete(prompt) → str HttpxLLMClient ↔ FakeLLMClient ↔ другая модель
Репозиторий Протокол Repository (17 методов) PgRepository ↔ MemRepository (тесты)
Загрузка parse_multipart() — чистая функция Парсер не зависит от HTTP-фреймворка
Классификация classify_batch(batch_id, llm_client, repo) — Dependency Injection Любой LLM + любое хранилище

Dependency Injection

Все внешние зависимости (LLM, БД) пробрасываются через параметры, а не через глобальные import:


10. Тестирование 26/26 PASS

Модуль Тестов Что проверяют
Контракты + загрузка 13 Создание dataclass'ов, garbage-фильтр, multipart-парсинг, path-traversal
LLM + Репозиторий 9 FakeLLMClient, MemRepository CRUD, все 17 методов
Классификация 4 Garbage по имени файла, garbage по заголовкам, classify с FakeLLM, no pending

Все тесты проходят без доступа к БД и LLM — через FakeLLMClient (record/replay) и MemRepository. Запуск: pytest deploy/tests/ -v.


11. Стек технологий

Компонент Где Технология
Веб-интерфейс Managed Python (Nubes) Python 3.12, Jinja2, ванильный JS
Механизм обработки Выделенная VM Python 3.12, httpx, pdfplumber, python-docx
База данных VM PostgreSQL 15 JSONB, UUID, advisory locks
LLM api.aillm.ru gpt-oss-120b (бесплатно, OpenAI API)
Деплой Managed Python + VM Dockerfile, Nginx + Let's Encrypt

12. Конечная цель будущее

📋 Заказчик: «Конечная цель: построчное сравнение CRM ↔ фискальная система.
Особый фокус: даты начала услуг.
PAYG (суффикс -m) — особый случай. Сверку с CRM тоже можно делегировать AI.»

Текущий пайплайн завершается на этапе извлечения спецификации из договоров и отслеживания изменений по допникам. Модуль сравнения с CRM — спроектирован как CanonicalRow адаптер, ожидает формата данных от заказчика.


13. Что дальше требует участия Заказчика

📋 Заказчик: «Это всё пока опыты и набивание шишек.
Не надеюсь с первого захода на идеальный результат.»
  1. Золотой набор: 30–50 реальных документов с ручной разметкой — измерить точность LLM
  2. Формат CRM: в каком виде данные из системы учёта клиентов — для модуля сверки
  3. Приоритет: точность vs скорость обработки — влияет на выбор модели LLM (локальная vs облачная)