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

486 lines
29 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>
<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)">ZIP-бомба (архив 1 KB → 10 GB)</td>
<td style="padding:8px;border:1px solid var(--border)">Лимит 500 файлов, 500 MB, ratio сжатия ≤ 100×</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Path traversal в ZIP (<code>../../etc/passwd</code>)</td>
<td style="padding:8px;border:1px solid var(--border)">Проверка имени файла ДО <code>os.path.basename</code></td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Битые кодировки имён в ZIP</td>
<td style="padding:8px;border:1px solid var(--border)">cp437 → utf8 перекодировка</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Файлы не-Word/PDF (exe, картинки, etc.)</td>
<td style="padding:8px;border:1px solid var(--border)">Whitelist: только <code>pdf/docx/doc/zip</code> — остальные <code>unknown_format</code></td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Дубликаты файлов (тот же контент, другое имя)</td>
<td style="padding:8px;border:1px solid var(--border)"><code>sha256(original_bytes)</code> → пропуск с пометкой <code>duplicate</code></td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Дубликаты услуг (одна услуга в разных допниках)</td>
<td style="padding:8px;border:1px solid var(--border)">UPSERT по <code>name_hash</code> (имя + дата начала) — разные периоды = разные строки</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">ADD без названия услуги</td>
<td style="padding:8px;border:1px solid var(--border)"><code>UNRESOLVED</code>, не применяется</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">UPDATE/DELETE без идентификатора цели</td>
<td style="padding:8px;border:1px solid var(--border)">Пустой <code>target_hash</code><code>UNRESOLVED</code></td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Неизвестный тип операции от LLM</td>
<td style="padding:8px;border:1px solid var(--border)"><code>UNRESOLVED</code></td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Кривые числа (<code>1 000,50</code>)</td>
<td style="padding:8px;border:1px solid var(--border)">Авто-нормализация: пробелы → удалить, запятая → точка</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Кривые даты (<code>01.03.2026</code>)</td>
<td style="padding:8px;border:1px solid var(--border)">Авто-нормализация: DD.MM.YYYY → YYYY-MM-DD</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Арифметические ошибки</td>
<td style="padding:8px;border:1px solid var(--border)">Проверка <code>price × qty = sum</code> — расхождение → флаг</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Мусорные документы (счета, акты, платёжки)</td>
<td style="padding:8px;border:1px solid var(--border)">Трёхэтапный фильтр: имя файла → заголовки (2000 симв.) → LLM</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Тихие ошибки (<code>try/except: pass</code>)</td>
<td style="padding:8px;border:1px solid var(--border)">Везде <code>logging.warning</code> с контекстом</td>
</tr>
</table>
<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><strong>Встроенный редактор промптов</strong> с полным CRUD и историей версий:</p>
<ul>
<li>Создание, редактирование, удаление промптов — через веб-интерфейс</li>
<li>Активация версии — один клик, и она сразу используется LLM</li>
<li>История версий — все предыдущие редакции сохраняются, можно откатиться</li>
<li>Без правки кода и без редеплоя — юрист может сам экспериментировать с промптами</li>
</ul>
<hr>
<h2>8. Хранение документов <span class="tag tag-ok">конфиденциальность</span></h2>
<blockquote>
📋 <strong>Заказчик:</strong> «Загрузка из локали, НЕ S3 (конфиденциальность).»
</blockquote>
<p><strong>Исходные файлы (.docx/.pdf/.doc/zip) НЕ сохраняются в БД.</strong> После загрузки и парсинга они удаляются с сервера. В БД хранятся только:</p>
<ul>
<li>Метаданные: имя файла, дата, хеш содержимого</li>
<li>Результат парсинга: <code>elements_json</code> (текст и таблицы)</li>
<li>Результат классификации: тип, номер, дата, контрагент</li>
<li>События спецификации: ADD/UPDATE/DELETE</li>
</ul>
<p>Сами бинарные файлы на диске не остаются.</p>
<hr>
<h2>9. Инженерные практики <span class="tag tag-ok">качество кода</span></h2>
<h3>Decoupling — разделение ответственности</h3>
<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>
<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)">5 frozen dataclass'ов: <code>ParseResult</code>, <code>ClassifyResult</code>, <code>GroupingResult</code>, <code>BatchGroupingResult</code>, <code>CompareOp</code></td>
<td style="padding:8px;border:1px solid var(--border)">Формат данных не привязан к хранилищу</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">LLM-клиент</td>
<td style="padding:8px;border:1px solid var(--border)">Протокол <code>LLMClient.complete(prompt) → str</code></td>
<td style="padding:8px;border:1px solid var(--border)">HttpxLLMClient ↔ FakeLLMClient ↔ другая модель</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Репозиторий</td>
<td style="padding:8px;border:1px solid var(--border)">Протокол <code>Repository</code> (17 методов)</td>
<td style="padding:8px;border:1px solid var(--border)">PgRepository ↔ MemRepository (тесты)</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Загрузка</td>
<td style="padding:8px;border:1px solid var(--border)"><code>parse_multipart()</code> — чистая функция</td>
<td style="padding:8px;border:1px solid var(--border)">Парсер не зависит от HTTP-фреймворка</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Классификация</td>
<td style="padding:8px;border:1px solid var(--border)"><code>classify_batch(batch_id, llm_client, repo)</code> — Dependency Injection</td>
<td style="padding:8px;border:1px solid var(--border)">Любой LLM + любое хранилище</td>
</tr>
</table>
<h3>Dependency Injection</h3>
<p>Все внешние зависимости (LLM, БД) пробрасываются через параметры, а не через глобальные <code>import</code>:</p>
<ul>
<li><code>call_llm(prompt, llm_client=None)</code> — DI с обратной совместимостью</li>
<li><code>classify_batch(batch_id, llm_client=None, repo=None)</code> — можно подставить любой backend</li>
<li>В продакшене: HttpxLLMClient + PgRepository</li>
<li>В тестах: FakeLLMClient (record/replay) + MemRepository (в памяти)</li>
</ul>
<hr>
<h2>10. Тестирование <span class="tag tag-ok">26/26 PASS</span></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)">13</td>
<td style="padding:8px;border:1px solid var(--border)">Создание dataclass'ов, garbage-фильтр, multipart-парсинг, path-traversal</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">LLM + Репозиторий</td>
<td style="padding:8px;border:1px solid var(--border)">9</td>
<td style="padding:8px;border:1px solid var(--border)">FakeLLMClient, MemRepository CRUD, все 17 методов</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)">Классификация</td>
<td style="padding:8px;border:1px solid var(--border)">4</td>
<td style="padding:8px;border:1px solid var(--border)">Garbage по имени файла, garbage по заголовкам, classify с FakeLLM, no pending</td>
</tr>
</table>
<p>Все тесты проходят без доступа к БД и LLM — через <code>FakeLLMClient</code> (record/replay) и <code>MemRepository</code>. Запуск: <code>pytest deploy/tests/ -v</code>.</p>
<hr>
<h2>11. Изоляция окружений</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)">URL</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)"><strong>Продакшен</strong></td>
<td style="padding:8px;border:1px solid var(--border)"><code>contracts.kube5s.ru</code></td>
<td style="padding:8px;border:1px solid var(--border)"><code>contracts</code></td>
<td style="padding:8px;border:1px solid var(--border)">Рабочая система</td>
</tr>
<tr>
<td style="padding:8px;border:1px solid var(--border)"><strong>Тестовый</strong></td>
<td style="padding:8px;border:1px solid var(--border)"><code>check.kube5s.ru</code></td>
<td style="padding:8px;border:1px solid var(--border)"><code>contracts_check</code></td>
<td style="padding:8px;border:1px solid var(--border)">Отладка, эксперименты — не затрагивает продакшен</td>
</tr>
</table>
<p>Тестовое окружение — полная копия пайплайна на отдельном порту (8777) с отдельной БД. Можно загружать, классифицировать, сравнивать — не боясь испортить рабочие данные.</p>
<hr>
<h2>12. Стек технологий</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 Python (Nubes)</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</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 Python + VM</td>
<td style="padding:8px;border:1px solid var(--border)">Dockerfile, Nginx + Let's Encrypt</td>
</tr>
</table>
<hr>
<h2>13. Конечная цель <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>14. Что дальше <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>