519 lines
33 KiB
HTML
519 lines
33 KiB
HTML
<!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>
|
||
|
||
<svg viewBox="0 0 860 280" style="width:100%;max-width:860px;margin:16px 0;" xmlns="http://www.w3.org/2000/svg">
|
||
<defs>
|
||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#58a6ff"/></marker>
|
||
<linearGradient id="g1" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#1a3a2a"/><stop offset="100%" stop-color="#0d2818"/></linearGradient>
|
||
<linearGradient id="g2" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#1a2a3a"/><stop offset="100%" stop-color="#0d1a28"/></linearGradient>
|
||
<linearGradient id="g3" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#2a1a3a"/><stop offset="100%" stop-color="#1a0d28"/></linearGradient>
|
||
<linearGradient id="g4" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#3a2a1a"/><stop offset="100%" stop-color="#281a0d"/></linearGradient>
|
||
<filter id="shadow"><feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.3"/></filter>
|
||
</defs>
|
||
|
||
<!-- Row 1: Pipeline -->
|
||
<!-- 1. Upload -->
|
||
<rect x="10" y="30" width="120" height="80" rx="8" fill="url(#g1)" stroke="#3fb950" stroke-width="1.5" filter="url(#shadow)"/>
|
||
<text x="70" y="60" text-anchor="middle" fill="#3fb950" font-size="13" font-weight="bold">📤 Загрузка</text>
|
||
<text x="70" y="80" text-anchor="middle" fill="#8b949e" font-size="10">.docx .pdf .doc .zip</text>
|
||
<text x="70" y="96" text-anchor="middle" fill="#8b949e" font-size="9">Whitelist форматов</text>
|
||
|
||
<line x1="132" y1="70" x2="160" y2="70" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||
|
||
<!-- 2. Parse -->
|
||
<rect x="165" y="30" width="120" height="80" rx="8" fill="url(#g2)" stroke="#58a6ff" stroke-width="1.5" filter="url(#shadow)"/>
|
||
<text x="225" y="60" text-anchor="middle" fill="#58a6ff" font-size="13" font-weight="bold">🔍 Парсинг</text>
|
||
<text x="225" y="80" text-anchor="middle" fill="#8b949e" font-size="10">pdfplumber</text>
|
||
<text x="225" y="96" text-anchor="middle" fill="#8b949e" font-size="10">python-docx</text>
|
||
|
||
<line x1="287" y1="70" x2="315" y2="70" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||
|
||
<!-- 3. Classify -->
|
||
<rect x="320" y="30" width="130" height="80" rx="8" fill="url(#g3)" stroke="#bc8cff" stroke-width="1.5" filter="url(#shadow)"/>
|
||
<text x="385" y="55" text-anchor="middle" fill="#bc8cff" font-size="13" font-weight="bold">🏷️ Классификация</text>
|
||
<text x="385" y="73" text-anchor="middle" fill="#8b949e" font-size="9">3-этапный фильтр</text>
|
||
<text x="385" y="88" text-anchor="middle" fill="#8b949e" font-size="9">имя → заголовки → LLM</text>
|
||
<text x="385" y="103" text-anchor="middle" fill="#f85149" font-size="8">мусор отсеивается</text>
|
||
|
||
<line x1="452" y1="70" x2="480" y2="70" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||
|
||
<!-- 4. Group -->
|
||
<rect x="485" y="30" width="120" height="80" rx="8" fill="url(#g4)" stroke="#d2991d" stroke-width="1.5" filter="url(#shadow)"/>
|
||
<text x="545" y="60" text-anchor="middle" fill="#d2991d" font-size="13" font-weight="bold">📋 Группировка</text>
|
||
<text x="545" y="80" text-anchor="middle" fill="#8b949e" font-size="10">по номерам</text>
|
||
<text x="545" y="96" text-anchor="middle" fill="#8b949e" font-size="10">договоров</text>
|
||
|
||
<line x1="607" y1="70" x2="635" y2="70" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||
|
||
<!-- 5. Compare -->
|
||
<rect x="640" y="30" width="130" height="80" rx="8" fill="url(#g2)" stroke="#58a6ff" stroke-width="1.5" filter="url(#shadow)"/>
|
||
<text x="705" y="55" text-anchor="middle" fill="#58a6ff" font-size="13" font-weight="bold">🤖 LLM-сравнение</text>
|
||
<text x="705" y="73" text-anchor="middle" fill="#8b949e" font-size="9">extract + diff</text>
|
||
<text x="705" y="88" text-anchor="middle" fill="#8b949e" font-size="9">gpt-oss-120b</text>
|
||
<text x="705" y="103" text-anchor="middle" fill="#f85149" font-size="8">ADD UPDATE DELETE</text>
|
||
|
||
<!-- Row 2: Protection layer -->
|
||
<rect x="10" y="140" width="860" height="55" rx="8" fill="#1a0a0a" stroke="#30363d" stroke-width="1" stroke-dasharray="4,3"/>
|
||
<text x="440" y="162" text-anchor="middle" fill="#f85149" font-size="12" font-weight="bold">🛡️ Защита на каждом этапе</text>
|
||
<text x="440" y="182" text-anchor="middle" fill="#8b949e" font-size="10">ZIP-бомба (500 файлов, 500MB, ratio 100×) | Path traversal | Whitelist форматов | content_hash дедупликация | UNRESOLVED для битых данных | авто-нормализация чисел/дат | арифметическая проверка</text>
|
||
|
||
<line x1="440" y1="128" x2="440" y2="138" stroke="#f85149" stroke-width="1" stroke-dasharray="2,2"/>
|
||
|
||
<!-- Row 3: Event Sourcing output -->
|
||
<rect x="10" y="220" width="860" height="40" rx="8" fill="#161b22" stroke="#3fb950" stroke-width="1.5" filter="url(#shadow)"/>
|
||
<text x="440" y="245" text-anchor="middle" fill="#3fb950" font-size="12" font-weight="bold">📊 Результат: spec_events (аудит) → spec_current (текущая спецификация) | Промпты с версионированием и редактором | Промежуточные результаты по каждому файлу</text>
|
||
</svg>
|
||
|
||
<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>
|
||
<p><strong>В БД сохраняются</strong> — это и есть рабочие данные системы:</p>
|
||
<ul>
|
||
<li>Метаданные: имя файла, дата, хеш содержимого</li>
|
||
<li>Результат парсинга: <code>elements_json</code> (текст и таблицы)</li>
|
||
<li>Результат классификации: тип, номер, дата, контрагент</li>
|
||
<li>События спецификации: ADD/UPDATE/DELETE</li>
|
||
</ul>
|
||
|
||
<p style="color:var(--orange);"><strong>⚠️ Пока не реализовано:</strong> удаление промежуточных данных после сессии. Варианты: кнопка «Очистить» в интерфейсе, либо Redis с TTL. Надо решать.</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)">Где</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>12. Конечная цель <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>13. Что дальше <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>
|