Files
contracts-flask/site/templates/architect.html
T

304 lines
18 KiB
HTML
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.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Архитектура — Сверка договоров</title>
<style>
:root {
--bg: #0d1117; --fg: #c9d1d9; --muted: #8b949e;
--accent: #58a6ff; --green: #3fb950; --orange: #d2991d;
--yellow: #e3b341; --red: #f85149; --border: #30363d;
--code-bg: #161b22; --card-bg: #161b22;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font: 15px/1.65 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--fg); max-width: 860px; margin: 0 auto; padding: 32px 20px 80px; }
h1 { font-size: 26px; margin-bottom: 8px; }
h2 { font-size: 19px; margin-top: 36px; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid var(--border); color: var(--accent); }
h3 { font-size: 16px; margin-top: 24px; margin-bottom: 6px; color: var(--green); }
p { margin: 8px 0; }
blockquote { border-left: 3px solid var(--orange); margin: 12px 0; padding: 8px 16px; background: var(--card-bg); border-radius: 0 6px 6px 0; font-style: italic; color: var(--yellow); }
blockquote strong { color: var(--orange); }
ul, ol { padding-left: 24px; margin: 8px 0; }
li { margin: 4px 0; }
code { background: var(--code-bg); padding: 1px 5px; border-radius: 4px; font-size: 13px; border: 1px solid var(--border); }
pre { background: var(--code-bg); padding: 12px 16px; border-radius: 6px; overflow-x: auto; font-size: 13px; border: 1px solid var(--border); margin: 12px 0; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 12px; font-weight: 600; margin-right: 6px; }
.tag-ok { background: #1a3a2a; color: var(--green); }
.tag-llm { background: #1a2a3a; color: var(--accent); }
.tag-warn { background: #3a2a1a; color: var(--orange); }
.tag-future { background: #2a1a3a; color: #bc8cff; }
.nav { margin-bottom: 24px; }
.nav a { color: var(--accent); text-decoration: none; }
.nav a:hover { text-decoration: underline; }
hr { border: none; border-top: 1px solid var(--border); margin: 32px 0; }
</style>
</head>
<body>
<div class="nav"><a href="/">← Вернуться к загрузке</a></div>
<h1>🏗️ Архитектура — сверка договоров</h1>
<p style="color:var(--muted)">Пайплайн: загрузка → парсинг → классификация → группировка → сравнение → аудит</p>
<!--
Все комментарии ниже — прямые цитаты или пересказ пожеланий Заказчика (Сергей Мищук) из переписки 2025–2026.
-->
<hr>
<h2>1. Загрузка файлов</h2>
<p>Пользователь выбирает <code>.docx</code> / <code>.pdf</code> / <code>.doc</code> / <code>.zip</code>.</p>
<ul>
<li><strong>PDF:</strong> <code>pdfplumber</code> извлекает текст и таблицы с сохранением структуры колонок</li>
<li><strong>DOCX:</strong> <code>python-docx</code> читает XML — извлекает параграфы и таблицы построчно</li>
<li><strong>DOC:</strong> обрабатывается как DOCX (python-docx читает большинство .doc)</li>
<li><strong>ZIP:</strong> архив распаковывается с лимитом 500 файлов и проверкой на сжатие (защита от ZIP-бомбы). Каждый файл внутри проходит тот же путь загрузки</li>
<li>Файлы не поддерживаемых форматов — отклоняются с пометкой <code>unknown_format</code></li>
</ul>
<p><strong>Результат:</strong> <code>elements_json</code> — массив элементов двух типов:</p>
<ul>
<li><code>{type: "paragraph", text: "...", style: "..."}</code></li>
<li><code>{type: "table", rows: [["колонка1", "колонка2"], ...]}</code></li>
</ul>
<p>Документ сохраняется в БД, считается <code>content_hash</code> (SHA-256) — если в этом же батче уже есть файл с таким содержимым, он помечается как <code>duplicate</code> и не дублируется.</p>
<blockquote>
📋 <strong>Заказчик:</strong> «100+ файлов за раз. Файлы: .docx/.pdf, возможно ZIP.<br>
Загрузка из локали (файловая шара / облачный диск), НЕ S3 (конфиденциальность).»
</blockquote>
<hr>
<h2>2. Классификация документов</h2>
<p>Каждый загруженный файл проходит <strong>трёхэтапный фильтр</strong>.</p>
<blockquote>
📋 <strong>Заказчик:</strong> «Оставлять только: договоры, доп. соглашения, спецификации.<br>
Игнорировать: акты сверки, счета/счета-фактуры/УПД, акты услуг, платёжки.»
</blockquote>
<h3>Этап 1 — имя файла <span class="tag tag-ok">0 токенов</span></h3>
<p>Проверка имени файла на мусорные ключевые слова: «счёт», «акт», «платёж», «УПД», «сверка», «invoice» и др. Совпадение → <code>garbage</code>, дальше не идёт.</p>
<h3>Этап 2 — заголовки в тексте <span class="tag tag-ok">0 токенов</span></h3>
<p>Проверка первых 2000 символов текста на заголовки: «СЧЕТ-ФАКТУРА», «АКТ СВЕРКИ», «АКТ ОКАЗАННЫХ УСЛУГ» и т.п. Совпадение → <code>garbage</code>.</p>
<blockquote>
📋 <strong>Заказчик:</strong> «Названия документов неинформативные. Есть лишние документы.<br>
По всем контрагентам.»
</blockquote>
<h3>Этап 3 — LLM <span class="tag tag-llm">LLM</span></h3>
<p>Оставшиеся документы отправляются в LLM. Модель получает выжимку текста (первые 1500 символов + строки с маркерами «договор», «соглашение», «спецификация») и возвращает JSON:</p>
<ul>
<li><code>doc_type</code><code>contract</code> / <code>supplement</code> / <code>specification</code> / <code>other</code></li>
<li><code>own_number</code> — номер этого документа</li>
<li><code>parent_number</code> — номер родительского договора (для допников и спецификаций)</li>
<li><code>doc_date</code> — дата документа YYYY-MM-DD</li>
<li><code>counterparty</code> — название контрагента</li>
</ul>
<blockquote>
📋 <strong>Заказчик:</strong> «НУБЕС = Исполнитель во всех целевых договорах.»<br>
→ counterparty = <strong>другая сторона</strong> (Заказчик).
</blockquote>
<hr>
<h2>3. Группировка по договорам</h2>
<p><strong>Детерминированная (без LLM).</strong> Документы группируются по номерам договоров.</p>
<blockquote>
📋 <strong>Заказчик:</strong> «Система сама должна разбираться, кто к кому.<br>
зачем нам базовый договор? Вся информация есть в допниках и спеках.»<br>
«Я не должен распознавать ничего. Система должна сделать всё.»
</blockquote>
<ul>
<li>Номера нормализуются: uppercase, удаляются все символы кроме букв и цифр.<br>
«МЭС-123/2024» → «МЭС1232024»</li>
<li>Допники и спецификации с одинаковым <code>parent_number</code> попадают в <strong>одну виртуальную группу</strong> — даже если сам договор не загружен</li>
<li>Неопознанные файлы → отдельная группа «Не распознано» с указанием причины</li>
</ul>
<hr>
<h2>4. Сравнение — извлечение + дифф</h2>
<p>Для каждой группы запускается последовательная обработка.</p>
<blockquote>
📋 <strong>Заказчик:</strong> «Важно. В произвольной, не повторяющейся.» (порядок допников)<br>
«Главное — точность разбора, не UI. Сначала базовая функция → потом юзабельность.»
</blockquote>
<h3>4a. Сортировка</h3>
<p>Документы внутри группы сортируются по <code>doc_date</code> (из классификации), затем по времени загрузки.</p>
<h3>4b. Базовый договор (первый в группе) <span class="tag tag-llm">LLM</span></h3>
<ul>
<li>Текст спецификации услуг → LLM с промптом <code>extract</code></li>
<li>LLM получает текст и инструкцию «найди таблицу услуг, извлеки каждую строку как ADD»</li>
<li>Ответ: массив операций ADD — каждая услуга с полями: название, цена, количество, сумма, дата начала</li>
</ul>
<h3>4c. Дополнительные соглашения <span class="tag tag-llm">LLM</span></h3>
<ul>
<li>Текущая спецификация + текст допника → LLM с промптом <code>diff</code></li>
<li>LLM определяет изменения:
<ul>
<li><code>ADD</code> — новая услуга</li>
<li><code>UPDATE</code> — изменение цены/количества/суммы</li>
<li><code>DELETE</code> — услуга исключена</li>
</ul>
</li>
</ul>
<h3>4d. Режим «изложить в новой редакции»</h3>
<p>Если допник говорит «изложить в новой редакции» — LLM возвращает полный новый список (<code>full_replace</code>). Все старые строки заменяются новыми.</p>
<hr>
<h2>5. Защита и валидация <span class="tag tag-warn">валидация</span></h2>
<ul>
<li><code>ADD</code> без имени услуги → <code>UNRESOLVED</code> (не применяется, помечается для ручной проверки)</li>
<li><code>UPDATE</code>/<code>DELETE</code> с пустым идентификатором → <code>UNRESOLVED</code></li>
<li><strong>Арифметика:</strong> <code>sum = price × qty</code> — расхождение → флаг</li>
<li><strong>Дубликаты:</strong> <code>content_hash</code> — один и тот же файл дважды не обрабатывается</li>
<li><strong>Даты:</strong> <code>01.03.2026</code><code>2026-03-01</code> (авто-нормализация)</li>
<li><strong>Числа:</strong> <code>1 000,50</code><code>1000.50</code> (авто-нормализация)</li>
</ul>
<blockquote>
📋 <strong>Заказчик:</strong> «кривых документов можно ожидать.<br>
Если не может разобраться — пусть зовет на помощь юзера.»
</blockquote>
<hr>
<h2>6. Event Sourcing — аудит <span class="tag tag-ok">аудит</span></h2>
<blockquote>
📋 <strong>Заказчик:</strong> «Наверно я захочу иметь возможность посмотреть любые промежуточные результаты.<br>
Было бы хорошо пояснить или показать процесс, что в каком порядке происходит.»
</blockquote>
<ul>
<li><strong>spec_events:</strong> полный лог всех операций. Каждое событие содержит:
<ul>
<li>тип (<code>ADD/UPDATE/DELETE/UNRESOLVED</code>)</li>
<li>источник — какой документ</li>
<li>версия промпта</li>
<li>полный сырой ответ LLM</li>
<li>временную метку</li>
</ul>
</li>
<li><strong>spec_current:</strong> материализованное представление = сумма всех событий</li>
<li><strong>Промежуточные результаты:</strong> для каждого файла доступны:
<ul>
<li>текст, отправленный в LLM</li>
<li>сырой ответ LLM</li>
<li>распарсенный JSON операций</li>
<li>какие операции были применены</li>
</ul>
</li>
</ul>
<hr>
<h2>7. Промпты <span class="tag tag-llm">LLM</span></h2>
<p>Три роли промптов, хранятся в БД с версионированием:</p>
<table style="width:100%;border-collapse:collapse;margin:12px 0;border:1px solid var(--border)">
<tr style="background:var(--code-bg)">
<th style="padding:8px;text-align:left;border:1px solid var(--border)">Роль</th>
<th style="padding:8px;text-align:left;border:1px solid var(--border)">Назначение</th>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)"><code>extract</code></td>
<td style="padding:8px;border:1px solid var(--border)">Извлечение строк спецификации из договора</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)"><code>diff</code></td>
<td style="padding:8px;border:1px solid var(--border)">Сравнение допников с текущей спецификацией</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)"><code>classify</code></td>
<td style="padding:8px;border:1px solid var(--border)">Определение типа/номера/даты/контрагента документа</td>
</tr>
</table>
<p>Встроенный редактор с историей версий: сохранил новую версию → она сразу используется LLM. Старые версии остаются в истории — можно откатиться.</p>
<hr>
<h2>8. Стек технологий</h2>
<table style="width:100%;border-collapse:collapse;margin:12px 0;border:1px solid var(--border)">
<tr style="background:var(--code-bg)">
<th style="padding:8px;text-align:left;border:1px solid var(--border)">Компонент</th>
<th style="padding:8px;text-align:left;border:1px solid var(--border)">Где</th>
<th style="padding:8px;text-align:left;border:1px solid var(--border)">Технология</th>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Веб-интерфейс</td>
<td style="padding:8px;border:1px solid var(--border)">Managed Flask (k8s)</td>
<td style="padding:8px;border:1px solid var(--border)">Python 3.12, Jinja2, ванильный JS</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Механизм обработки</td>
<td style="padding:8px;border:1px solid var(--border)">VM (5.172.178.213)</td>
<td style="padding:8px;border:1px solid var(--border)">Python 3.12, httpx, pdfplumber, python-docx</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">База данных</td>
<td style="padding:8px;border:1px solid var(--border)">VM PostgreSQL 15</td>
<td style="padding:8px;border:1px solid var(--border)">JSONB, UUID, advisory locks</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">LLM</td>
<td style="padding:8px;border:1px solid var(--border)">api.aillm.ru</td>
<td style="padding:8px;border:1px solid var(--border)">gpt-oss-120b (бесплатно, OpenAI API)</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Деплой</td>
<td style="padding:8px;border:1px solid var(--border)">Managed k8s + VM</td>
<td style="padding:8px;border:1px solid var(--border)">Dockerfile, Nginx + Let's Encrypt</td>
</tr>
</table>
<hr>
<h2>9. Конечная цель <span class="tag tag-future">будущее</span></h2>
<blockquote>
📋 <strong>Заказчик:</strong> «Конечная цель: построчное сравнение CRM ↔ фискальная система.<br>
Особый фокус: даты начала услуг.<br>
PAYG (суффикс -m) — особый случай. Сверку с CRM тоже можно делегировать AI.»
</blockquote>
<p>Текущий пайплайн завершается на этапе <strong>извлечения спецификации из договоров и отслеживания изменений по допникам</strong>. Модуль сравнения с CRM — спроектирован как <code>CanonicalRow</code> адаптер, ожидает формата данных от заказчика.</p>
<hr>
<h2>10. Что дальше <span class="tag tag-future">требует участия Заказчика</span></h2>
<blockquote>
📋 <strong>Заказчик:</strong> «Это всё пока опыты и набивание шишек.<br>
Не надеюсь с первого захода на идеальный результат.»
</blockquote>
<ol>
<li><strong>Золотой набор:</strong> 30–50 реальных документов с ручной разметкой — измерить точность LLM</li>
<li><strong>Формат CRM:</strong> в каком виде данные из системы учёта клиентов — для модуля сверки</li>
<li><strong>Приоритет:</strong> точность vs скорость обработки — влияет на выбор модели LLM (локальная vs облачная)</li>
</ol>
</body>
</html>