docs: архитектурный анализ + History + gitignore (2026-06-27)

This commit is contained in:
“Naeel”
2026-06-27 13:00:18 +04:00
parent 4a21d77f51
commit 82c5c075f1
154 changed files with 4789 additions and 1443 deletions
@@ -0,0 +1,795 @@
# Архитектурное исследование: Сверка договоров v2
**Дата:** 27.06.2026 | **Для:** DeepSeek V4 Pro | **По заказу:** Владимир Крупский
---
## Блок 1: Общая архитектура
### 1.1 Архитектура «с нуля»
Вот как бы я построил систему, зная все требования сейчас:
```mermaid
graph TB
subgraph "Ввод"
A[Файловая шара<br>/облачный диск]
end
subgraph "Pre-processing Pipeline"
B["① Фильтр мусора<br>━━━━━━━━━━━━━<br>Детерминированная<br>(ключевые слова + regex<br>по первым 2KB текста)"]
C["② Парсинг документов<br>━━━━━━━━━━━━━<br>Python (pdfplumber + python-docx)<br>→ elements_json"]
D["③ Классификация<br>━━━━━━━━━━━━━<br>LLM (лёгкая модель)<br>+ _smart_extract<br>→ тип/номер/дата/контрагент"]
E["④ Группировка<br>━━━━━━━━━━━━━<br>Детерминированная Python<br>нормализация номеров + matching"]
end
subgraph "Core Processing"
F["⑤ Извлечение спецификации<br>━━━━━━━━━━━━━<br>LLM (основная модель)<br>контекст: полный текст<br>договора/спецификации<br>→ ADD ops"]
G["⑥ Сравнение ДС<br>━━━━━━━━━━━━━<br>LLM + Event Sourcing<br>контекст: текущая spec<br>+ текст ДС<br>→ ADD/UPDATE/DELETE/UNRESOLVED"]
end
subgraph "Сверка"
H["⑦ Matching CRM ↔ Фискальная<br>━━━━━━━━━━━━━<br>Гибрид: хеш-матчинг по<br>нормализованному имени<br>+ LLM для несовпадений"]
I["⑧ Отчёт о расхождениях<br>━━━━━━━━━━━━━<br>diff-представление<br>подсветка: даты, цены, суммы"]
end
subgraph "Хранилище"
J[(PostgreSQL<br>Документы, Спецификации,<br>События, Промпты)]
K[(Доп. хранилище<br>CRM-выгрузки<br>agnostic schema)]
end
subgraph "Обратная связь"
L[Ручная коррекция<br>экспертом]
M[Версионирование<br>исправленных промптов]
end
A --> B --> C --> D --> E --> F --> G
G --> H --> I
J --- F
J --- G
K --- H
I --> L --> M
M -.-> F
M -.-> G
style B fill:#e8f5e9
style E fill:#e8f5e9
style D fill:#fff3e0
style F fill:#fff3e0
style G fill:#fff3e0
style H fill:#e3f2fd
```
**Зоны ответственности:**
| Компонент | Где LLM | Где детерминированная логика |
|---|---|---|
| ① Фильтр мусора | ❌ НЕТ | Ключевые слова + regex по заголовкам (быстро, 0 токенов) |
| ② Парсинг | ❌ НЕТ | pdfplumber / python-docx → `elements_json` |
| ③ Классификация | ✅ ЛЁГКАЯ LLM | `_smart_extract()` выжимка, `_safe_json_parse()` |
| ④ Группировка | ❌ НЕТ | `normalize_number()` + matching по parent_number |
| ⑤ Извлечение | ✅ ОСНОВНАЯ LLM | Сборка промпта (`build_prompt`), `_elements_to_text()` |
| ⑥ Сравнение | ✅ ОСНОВНАЯ LLM | Event Sourcing (apply ops), `_upsert_spec_current()` |
| ⑦ Matching | ✅ LLM для несовпадений | Хеш-матчинг по нормализованному имени для очевидных |
| ⑧ Отчёт | ❌ НЕТ | Чистый diff, группировка расхождений по типам |
**Ключевой принцип:** LLM — только там, где нужна семантика. Всё остальное — быстрый детерминированный код. Это даёт:
- Предсказуемость (детерминированное не ломается при смене модели)
- Экономию токенов (LLM — дорого и медленно)
- Отлаживаемость (можно тестировать unit-тестами без LLM)
---
### 1.2 Agent-based vs Pipeline
```mermaid
graph LR
subgraph "Pipeline (текущий)"
P1[Upload] --> P2[Parse] --> P3[Classify] --> P4[Group] --> P5[Compare]
end
subgraph "Agent-based (предлагаемый гибрид)"
O[Orchestrator Agent]
O --> W1[Parse Worker]
O --> W2[Classify Worker]
O --> W3[Compare Worker]
O --> W4[Match Worker]
O --> T[Tools: DB, LLM, FileSystem]
end
```
| Критерий | Pipeline | Agent-based |
|---|---|---|
| **Плюсы** | Предсказуемый порядок, легче отлаживать, меньше токенов, детерминированные шаги не требуют LLM | Гибкость: оркестратор решает **что делать** на основе промежуточных результатов. Может перепланировать при ошибках |
| **Минусы** | Жёсткая последовательность. Если шаг упал — либо пропускаем, либо всё стоп. Трудно адаптировать под неожиданные форматы документов | Дороже (каждый шаг оркестратора — LLM-вызов). Сложнее отлаживать. Риск «галлюцинаций» оркестратора |
| **Когда** | Когда формат входа **известен**, pipeline стабилен | Когда формат входа **неизвестен**, нужно адаптивное поведение |
**Мой вердикт: ГИБРИДНЫЙ подход.**
```
Orchestrator (лёгкая LLM, ~500 токенов/вызов)
├── «Это договор?» → Фильтр мусора (детерминирован)
├── «Какой тип?» → Classify worker (LLM, уже есть)
├── «С чем группировать?» → Grouping (детерминирован)
├── «Извлечь спецификацию?» → Extract worker (LLM)
└── «Сравнить с CRM?» → Match worker (LLM + хеши)
```
**Почему не pure agents:**
- 100+ файлов × 500 токенов оркестратора = 50K+ токенов только на планирование
- Для **стандартных** договоров ЦОД pipeline предсказуем — хватит 95% случаев
- Оркестратор нужен только для **краевых случаев**: нестандартный формат, ошибка парсинга, конфликт при группировке
**Инструменты (tools) для агента:**
- `parse_document(file_id)` → elements_json
- `classify_document(file_id)` → {type, number, date, counterparty}
- `extract_spec(contract_id)` → [spec_rows]
- `compare_supplement(supp_id, current_spec)` → [ops]
- `match_crm_row(spec_row)` → {crm_match, confidence}
- `query_db(sql)` → rows (read-only)
---
### 1.3 RAG — нужен ли?
**Кратко: для текущей задачи RAG НЕ НУЖЕН. Хватит контекстного окна.**
Обоснование:
| Что | Почему не RAG |
|---|---|
| **Текст одного допника** | 5-50 KB → влезает в контекстное окно gpt-oss-120b (8K токенов ≈ ~24KB текста) |
| **Текущая спецификация** | 10-50 строк × ~200 симв = 10KB → тоже влезает |
| **Сравнение договоров** | Не semantic search. Нужно **точное** сопоставление строк, а не «похожие документы» |
**Когда RAG стал бы нужен (v3+):**
- Если бы нужно было искать **похожие прецеденты** в истории (как раньше решали похожие расхождения)
- Если бы был корпус из 10 000+ договоров и нужно было искать «как обычно формулируют услугу X»
- Для чата: «покажи все договоры где цена стойко-места > 50 000»
**Что где хранить:**
| Хранилище | Что |
|---|---|
| **PostgreSQL (реляционная)** | Документы, `elements_json`, `spec_current`, `spec_events`, контракты, промпты, CRM-выгрузки |
| **Файловая система** | Исходные .docx/.pdf (для перепарсивания при смене парсера) |
| **Векторная БД** (пока НЕ нужно) | Эмбеддинги названий услуг для семантического matching (альтернатива LLM-matching) |
**Но:** если модель сменится на что-то с окном 128K+ токенов (Claude, GPT-4o, Gemini), можно будет отправлять **весь договор целиком** + текущую спецификацию. Тогда `_smart_extract` станет не нужен — LLM сама найдёт нужные строки.
---
## Блок 2: Обработка 100+ файлов
### 2.1 Узкие места и масштабирование
```mermaid
gantt
title Время обработки 100 файлов (текущий pipeline)
dateFormat X
axisFormat %s
section Фильтр мусора
100 файлов × 10ms :0, 1
section Парсинг
100 файлов × 500ms :1, 50
section Классификация
100 файлов × 2-10s :50, 300
section Группировка
1 вызов × 100ms :300, 300
section Сравнение (LLM)
30 допников × 30s :300, 900
```
**Главное узкое место — КЛАССИФИКАЦИЯ (50-300s для 100 файлов при 4 воркерах).**
Текущий `ThreadPoolExecutor(max_workers=4)` + `classify_worker.py` subprocess — уже правильное решение. Но для 100+ файлов:
**Что ещё станет узким местом:**
| Узкое место | Почему | Решение |
|---|---|---|
| **Классификация** | 100 файлов × 5s / 4 воркера = 125s | Увеличить `MAX_WORKERS` до 8-10 (но риск троттлинга api.aillm.ru) |
| **Сравнение ДС** | 30 допников × 30s последовательно = 900s (15 мин!) | Параллельное сравнение **независимых** групп (разные contract_id — нет гонки) |
| **Парсинг docx/pdf** | Java POI через Lucee — медленно для 100 файлов | Перенести парсинг на ВМ (pdfplumber/python-docx, см. ниже) |
| **Память процесса** | 100 `elements_json` в памяти БД | Ок — в БД, не в памяти питона |
| **api.aillm.ru rate limit** | Бесплатный эндпоинт, неизвестный лимит | Семафор + exponential backoff |
**Предлагаемые улучшения:**
1. **Перенос парсинга на ВМ** (убрать зависимость от Lucee/Java):
```
СЕЙЧАС: JS → convert_server → Lucee parser.cfm (Java POI/PDFBox) → обратно на ВМ
ПРЕДЛОЖЕНИЕ: JS → convert_server → services/parse.py (pdfplumber + python-docx)
```
- python-docx для .docx (чистый Python, без Java)
- pdfplumber для .pdf (лучше PDFBox для таблиц)
- Убирает latency сетевого вызова Lucee → ВМ
2. **Асинхронная очередь классификации:**
```
Upload → Parse → [положить в очередь] → сразу вернуть «файлы загружены»
→ background: classify → group
```
Заказчик не ждёт 125 секунд. Видит прогресс-бар через `/api/batch-progress`.
3. **Параллельное сравнение групп:**
```python
# Сейчас: последовательно по всем supps
for s in supps: # 30 допников × 30s = 900s
compare(s)
# Предложение: параллельно по НЕЗАВИСИМЫМ группам
with ThreadPoolExecutor(max_workers=3) as pool:
futures = {pool.submit(compare_group, g): g for g in independent_groups}
```
Группы с разными `contract_id` независимы — можно сравнивать параллельно.
---
### 2.2 Очередь (RabbitMQ/Redis/Kafka) или PostgreSQL?
| Критерий | PostgreSQL (текущий) | Redis | RabbitMQ |
|---|---|---|---|
| **Простота** | ✅ Уже есть, не надо ставить | Средне | Средне |
| **Надёжность** | ✅ ACID, не теряем задачи | ❌ Может потерять при перезапуске | ✅ Persistence |
| **Мониторинг** | ✅ SELECT для просмотра очереди | Нужен redis-cli | Нужен management plugin |
| **Производительность** | Средне (polling) | ✅ Высокая (pub/sub) | ✅ Высокая |
| **Подходит для** | **До 1000 файлов/день** | До 10K/день | До 100K/день |
**Вердикт: PostgreSQL ДОСТАТОЧНО для текущего масштаба.**
Для 100+ файлов за раз, несколько раз в неделю — PostgreSQL-очередь через `documents.classify_status = 'pending'` + `classify_worker.py` subprocess — адекватное решение. Не надо усложнять.
**Когда переходить на RabbitMQ/Redis:**
- Если заказчик начнёт загружать 1000+ файлов **ежедневно**
- Если появятся **несколько воркеров** на разных машинах
- Если нужен **приоритет** (срочные договоры вне очереди)
**Предлагаемая схема на PostgreSQL (минимальные изменения):**
```sql
-- Добавляем поле для очереди
ALTER TABLE documents ADD COLUMN IF NOT EXISTS classify_priority INT DEFAULT 0;
ALTER TABLE documents ADD COLUMN IF NOT EXISTS classify_attempts INT DEFAULT 0;
ALTER TABLE documents ADD COLUMN IF NOT EXISTS classify_next_attempt TIMESTAMPTZ;
-- Воркер забирает задачи с ORDER BY priority, attempt, next_attempt
SELECT * FROM documents
WHERE classify_status = 'pending'
AND (classify_next_attempt IS NULL OR classify_next_attempt <= NOW())
ORDER BY classify_priority DESC, classify_attempts ASC
LIMIT 10
FOR UPDATE SKIP LOCKED; -- конкурентное потребление
```
---
### 2.3 «Мусорная» фильтрация на раннем этапе
**Это КРИТИЧЕСКИ важно для 100+ файлов.** Если 50% файлов — счета/акты/платёжки, а мы их парсим и классифицируем — тратим 50% ресурсов впустую.
**Предлагаемый трёхэтапный фильтр:**
```
Этап 1: Фильтр по имени файла (0ms, детерминирован)
├── regex: (сч[её]т|акт|плат[её]ж|УПД|сверк|инвойс|invoice|act|payment)
├── сразу помечать doc_type='garbage', НЕ парсить
└── точность: ~40% мусора
Этап 2: Фильтр по первым 2KB текста (после парсинга, 10ms, детерминирован)
├── Ключевые слова в заголовке:
│ «СЧЕТ-ФАКТУРА», «АКТ СВЕРКИ», «АКТ оказанных услуг»,
│ «ПЛАТЁЖНОЕ ПОРУЧЕНИЕ», «УПД», «СЧЕТ НА ОПЛАТУ»
├── Если нашли → помечать doc_type='garbage', НЕ классифицировать LLM
└── точность: ~55% мусора (суммарно)
Этап 3: LLM-классификация (оставшиеся, 2-10s)
├── Только для файлов, прошедших этапы 1-2
└── LLM определяет точный тип: contract/supplement/specification/other
```
**Реализация этапа 2 (в `services/classify.py` перед `_call_llm_classify`):**
```python
GARBAGE_MARKERS = [
'СЧЕТ-ФАКТУРА', 'СЧЕТ НА ОПЛАТУ', 'АКТ СВЕРКИ', 'АКТ ОКАЗАННЫХ УСЛУГ',
'АКТ ВЫПОЛНЕННЫХ РАБОТ', 'ПЛАТЁЖНОЕ ПОРУЧЕНИЕ', 'УНИВЕРСАЛЬНЫЙ ПЕРЕДАТОЧНЫЙ',
'УПД', 'ПЛАТЕЖНОЕ ПОРУЧЕНИЕ'
]
def is_garbage_by_header(text):
"""Быстрая проверка — не гонять LLM на мусор."""
header = text[:2000].upper()
for marker in GARBAGE_MARKERS:
if marker in header:
return True
return False
```
**Экономия:** при 50% мусора в 100 файлах: вместо 100 LLM-вызовов (500s) → 50 LLM-вызовов (250s). **Экономия 50% времени и токенов.**
---
## Блок 3: Сравнение CRM ↔ фискальная система
### 3.1 Agnostic к источнику — проектирование модуля сравнения
**Проблема:** мы не знаем формат CRM. Сегодня — одна CRM, завтра — другая система.
**Решение: Абстрактный интерфейс + адаптеры.**
```mermaid
graph TB
subgraph "Источники данных"
CRM1[CRM<br>(текущая)]
CRM2[Другая система<br>(будущая)]
FISC[Фискальная система<br>(из договоров)]
end
subgraph "Адаптеры (по одному на источник)"
A1[CRM Adapter<br>нормализует поля<br>в канонический формат]
A2[Future Adapter]
end
subgraph "Каноническая модель строки"
CAN[CanonicalRow<br>────────────<br>service_name: str<br>article_code: str<br>price: Decimal<br>qty: Decimal<br>sum: Decimal<br>date_start: Date<br>date_end: Date<br>unit: str<br>source: 'crm' | 'fiscal'<br>source_id: str]
end
subgraph "Matcher (agnostic)"
MATCH[RowMatcher<br>────────────<br>match_by_hash<br>match_by_name<br>match_by_llm<br>→ MatchResult]
end
CRM1 --> A1 --> CAN
CRM2 --> A2 --> CAN
FISC --> CAN
CAN --> MATCH
```
**Каноническая модель `CanonicalRow`:**
```python
@dataclass
class CanonicalRow:
"""Строка спецификации в каноническом формате (source-agnostic)."""
service_name: str # нормализованное название услуги
article_code: str | None # артикул (если есть)
price: Decimal | None
qty: Decimal | None
sum: Decimal | None
date_start: date | None # КРИТИЧНОЕ ПОЛЕ
date_end: date | None
unit: str | None # кВт, шт., U, Мбит/с, ...
source: str # 'crm' | 'fiscal'
source_id: str # ссылка на оригинал (для аудита)
@property
def name_hash(self) -> str:
"""Нормализованный хеш названия для быстрого matching."""
return _hash(normalize_service_name(self.service_name))
```
**Адаптер для CRM (пример):**
```python
class CRMSourceAdapter:
"""Адаптер для конкретной CRM. Меняется только этот класс."""
def extract_rows(self, crm_export_path: str) -> list[CanonicalRow]:
"""Читает CSV/JSON/API CRM → список CanonicalRow."""
# Специфично для CRM заказчика
df = pd.read_csv(crm_export_path, sep=';')
rows = []
for _, r in df.iterrows():
rows.append(CanonicalRow(
service_name=r['Наименование'],
article_code=r.get('Артикул'),
price=Decimal(str(r['Цена'])),
qty=Decimal(str(r['Кол-во'])),
sum=Decimal(str(r['Сумма'])),
date_start=parse_date(r['Дата начала']),
date_end=parse_date(r.get('Дата окончания')),
unit=r.get('Ед. изм.'),
source='crm',
source_id=r['ID'],
))
return rows
```
**Ключевое:** когда завтра появится другая система — пишем **только новый адаптер**. Matcher не меняется.
---
### 3.2 Matching строк: CRM ↔ фискальная система
**Трёхуровневый matching (от быстрого к точному):**
```mermaid
graph LR
A[CRM строка] --> B{① Хеш-матчинг<br>по name_hash}
B -->|Совпал| D[✓ MATCH (100% confidence)]
B -->|Не совпал| C{② Семантический<br>по имени}
C -->|Высокая confidence| E[✓ MATCH (80-95% confidence)]
C -->|Низкая| F{③ LLM-матчинг}
F --> G[✓ MATCH / ✗ NO MATCH<br>+ объяснение]
```
**① Хеш-матчинг (0ms, 0 токенов):**
```python
def match_by_hash(crm_rows, fiscal_rows):
"""Точное совпадение по нормализованному имени."""
fiscal_by_hash = {r.name_hash: r for r in fiscal_rows}
matched = []
unmatched = []
for crm_row in crm_rows:
if crm_row.name_hash in fiscal_by_hash:
matched.append((crm_row, fiscal_by_hash[crm_row.name_hash], 1.0))
else:
unmatched.append(crm_row)
return matched, unmatched
```
**② Семантический matching (Python, без LLM):**
```python
def match_by_name_similarity(crm_row, fiscal_rows, threshold=0.8):
"""Fuzzy matching по названиям услуг."""
from difflib import SequenceMatcher
best_score = 0
best_match = None
for f_row in fiscal_rows:
score = SequenceMatcher(None,
crm_row.service_name.lower(),
f_row.service_name.lower()
).ratio()
if score > best_score:
best_score = score
best_match = f_row
if best_score >= threshold:
return best_match, best_score
return None, 0
```
**③ LLM-матчинг (для оставшихся ~10-20% сложных случаев):**
```
Промпт:
«Вот строка из CRM: {crm_row}
Вот строки из фискальной системы: {fiscal_rows}
Найди соответствие или скажи что соответствия нет.
Учитывай: синонимы ("аренда стойки" = "colocation"),
объединение/разделение строк,
PAYG-услуги без артикулов.»
```
**Почему не только LLM:** 100 строк × 500 токенов = 50K токенов только на matching. А ①+② обрабатывают 80% за 0 токенов.
---
### 3.3 Даты — критичный фокус
**Почему даты — главный источник расхождений (со слов заказчика):**
- CRM может иметь `date_start = 01.01.2025`
- Фискальная система (договор) может иметь `date_start = 15.01.2025` (дата подписания акта приёмки, а не договора)
- Разница в 14 дней → недоплата/переплата за 14 дней × стоимость услуги
**Стратегия сравнения с акцентом на `date_start`:**
```python
def compare_dates(crm_row, fiscal_row):
"""Сравнение дат — основной фокус."""
result = {
'matched': True,
'date_start_match': True,
'date_start_diff_days': 0,
'date_start_warning': None,
'price_match': True,
'sum_match': True,
}
# Сравнение дат
if crm_row.date_start and fiscal_row.date_start:
diff = (crm_row.date_start - fiscal_row.date_start).days
result['date_start_diff_days'] = diff
if diff != 0:
result['date_start_match'] = False
if abs(diff) <= 5:
result['date_start_warning'] = 'minor' # возможно округление до месяца
elif abs(diff) <= 31:
result['date_start_warning'] = 'significant' # расхождение на месяц
else:
result['date_start_warning'] = 'critical' # серьёзное расхождение
# Если даты не совпадают, но всё остальное совпадает (имя, цена, количество)
if not result['date_start_match'] and result['price_match'] and result['sum_match']:
result['likely_cause'] = 'date_input_error' # вероятно ошибка ввода даты
return result
```
**Визуализация расхождений (диаграмма Ганта):**
```
Услуга | Янв | Фев | Март | Апр |
────────────────────┼───────┼───────┼───────┼───────|
CRM: Стойка 10kW |████████████████|
Фискал: Стойка 10kW | ████████████████|
^^^^— расхождение 15 дней
```
Это можно отрендерить как HTML/CSS бары — наглядно видны сдвиги дат.
---
### 3.4 PAYG (суффикс `-m`) — стратегия сопоставления
**Проблема PAYG:**
- PAYG-услуги (pay-as-you-go) — переменное потребление, нет фиксированной цены
- В счетах нет кодов артикулов
- Например: «IP-адрес IPv4-m» в CRM vs «IP-адрес IPv4» в договоре
**Стратегия:**
```python
def normalize_payg_name(name):
"""Убирает суффикс -m для сопоставления PAYG-услуг."""
import re
# Убираем суффикс -m (с границей слова или концом строки)
normalized = re.sub(r'-m(\s|$)', r'\1', name)
# Примеры:
# «IP-адрес IPv4-m» → «IP-адрес IPv4»
# «Канал связи 100Мбит/с-m» → «Канал связи 100Мбит/с»
return normalized
```
**Алгоритм для PAYG:**
1. При matching по имени — нормализовать **оба** названия (убрать `-m`)
2. Если match нашёлся → отметить флагом `payg: true`
3. Для PAYG-услуг **не сравнивать суммы** (они переменные), сравнивать только **факт наличия услуги** и **единицу измерения**
4. Для PAYG-услуг `date_start` **особенно важен** — PAYG тарифицируется с даты начала
```python
def match_payg(crm_row, fiscal_rows):
"""Особая логика для PAYG."""
crm_name_normalized = normalize_payg_name(crm_row.service_name)
for f_row in fiscal_rows:
f_name_normalized = normalize_payg_name(f_row.service_name)
if crm_name_normalized == f_name_normalized:
return MatchResult(
matched=True,
payg=True,
compare_sum=False, # суммы не сравниваем для PAYG
compare_date_start=True, # даты КРИТИЧНЫ
note=f'PAYG: {crm_row.service_name} ↔ {f_row.service_name}',
)
return MatchResult(matched=False)
```
---
## Блок 4: Итеративность и неопределённость
### 4.1 Менять промпты/модели/подходы без переписывания кода
**Что уже есть (✅ хорошо):**
- Промпты в БД с версионированием (`prompts` таблица + `prompt.cfm`/`db/prompts.py`)
- Редактор промптов с историей версий
- Активный промпт выбирается из БД, не хардкод
**Что предлагаю добавить:**
```python
# config.py — ЕДИНСТВЕННОЕ место для конфигурации LLM
@dataclass
class LLMConfig:
"""Меняется без правки кода — через БД или env."""
model: str = os.environ.get("LLM_MODEL", "gpt-oss-120b")
url: str = os.environ.get("LLM_URL", "https://api.aillm.ru/v1/chat/completions")
max_tokens: int = int(os.environ.get("LLM_MAX_TOKENS", "8000"))
temperature: float = float(os.environ.get("LLM_TEMPERATURE", "0.1"))
timeout: int = int(os.environ.get("LLM_TIMEOUT", "120"))
# Разные модели для разных задач
classify_model: str = os.environ.get("LLM_CLASSIFY_MODEL", model) # полегче
extract_model: str = os.environ.get("LLM_EXTRACT_MODEL", model) # основная
match_model: str = os.environ.get("LLM_MATCH_MODEL", model) # для сверки
```
**Принцип: всё что может поменяться — в БД или env. Код — только движок.**
| Что меняется | Где менять | Без правки кода? |
|---|---|---|
| Промпт | БД `prompts` → activate | ✅ Да |
| Модель LLM | env `LLM_MODEL` | ✅ Да |
| Температура | env `LLM_TEMPERATURE` | ✅ Да |
| URL API | env `LLM_URL` | ✅ Да |
| Garbage-маркеры | `garbage_markers.json` в БД или файле | ✅ Да |
| Стратегия matching | `matching_rules` в БД | ✅ Да (если сделать rules engine) |
| Порядок pipeline | ❌ Пока хардкод | 🔧 Можно сделать DAG в БД |
**Предложение: Pipeline as DAG в БД:**
```sql
CREATE TABLE pipeline_steps (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL, -- 'filter_garbage', 'parse', 'classify', 'extract', ...
handler TEXT NOT NULL, -- 'services.classify:classify_batch'
depends_on INT[] DEFAULT '{}', -- какие шаги должны быть завершены
config JSONB DEFAULT '{}', -- параметры шага
enabled BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
Меняя записи в этой таблице, можно переставлять шаги или добавлять новые **без правки кода**.
---
### 4.2 MVP-границы: v1, v2, v3
```
v1 (MVP) — БАЗОВАЯ ФУНКЦИЯ
├── ✅ Загрузка 100+ файлов (из локали/файловой шары)
├── ✅ Фильтр мусора (этапы 1-2, детерминированные)
├── ✅ Парсинг docx/pdf на ВМ (убрать зависимость от Lucee)
├── ✅ Классификация (LLM, параллельно)
├── ✅ Группировка по контрагентам
├── ✅ Извлечение спецификации (LLM)
├── ✅ Сравнение ДС (LLM + Event Sourcing)
├── ✅ Выгрузка результата (CSV/JSON)
├── ❌ БЕЗ сверки с CRM (только извлечение из договоров)
├── ❌ БЕЗ красивого UI (минимальный интерфейс для отладки)
└── Домен: contracts.kube5s.ru
v2 — СВЕРКА
├── ✅ Адаптер CRM (первый источник)
├── ✅ Matching CRM ↔ фискальная (трёхуровневый)
├── ✅ Отчёт о расхождениях (diff)
├── ✅ Подсветка дат
├── ✅ PAYG-обработка
├── ✅ Ручная коррекция экспертом
└── Домен: check.kube5s.ru
v3 — ЮЗАБЕЛЬНОСТЬ
├── ✅ Красивый UI (две панели, отчёт)
├── ✅ Векторная БД для семантического поиска
├── ✅ Чат (Q&A по всем договорам)
├── ✅ Автообучение на коррекциях (few-shot из истории)
├── ✅ Экспорт в Excel
└── Домен: contracts.kube5s.ru (единый)
```
**Почему сверка с CRM — это v2, а не v1:**
- Заказчик сам говорит: «сейчас и с подходом всё неясно»
- Сначала надо доказать что LLM **вообще** может точно извлечь спецификацию из 100+ договоров
- Потом, имея эталонные данные, строить сверку
- Это снижает риск: не строим сложный matching для неподтверждённого качества извлечения
---
### 4.3 Цикл обратной связи
```mermaid
graph TB
A[LLM извлекает спецификацию] --> B[Результат показан эксперту]
B --> C{Эксперт: верно?}
C -->|✅ Да| D[Сохраняем как<br>положительный пример]
C -->|❌ Нет| E[Эксперт исправляет]
E --> F[Сохраняем пару<br>❌ было → ✅ стало]
F --> G[Аналитика ошибок<br>какие типы ошибок частые?]
G --> H{Можно исправить<br>промптом?}
H -->|Да| I[Правим промпт<br>новая версия]
H -->|Нет| J[Меняем подход<br>алгоритм / модель]
I --> K[Few-shot примеры<br>в промпт]
D --> K
K --> A
style C fill:#fff3e0
style G fill:#e3f2fd
```
**Конкретная реализация:**
```sql
-- Таблица коррекций эксперта
CREATE TABLE expert_corrections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_id UUID REFERENCES spec_events(id), -- какое событие LLM
corrected_values JSONB NOT NULL, -- что исправил эксперт
correction_type TEXT, -- 'name_fix', 'date_fix', 'price_fix', 'missing_row', 'extra_row'
expert_comment TEXT,
used_in_prompt BOOLEAN DEFAULT false, -- включено в few-shot?
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
**Как это использовать:**
1. **Быстрый цикл (часы):** эксперт исправил → сохранили в `expert_corrections` → аналитика показывает «5 из 10 ошибок — даты» → правим промпт (добавляем акцент на даты) → активируем новую версию → лучше.
2. **Few-shot обучение (дни):** накопили 20+ коррекций одного типа → добавляем в промпт как few-shot примеры:
```
ПРИМЕРЫ ОШИБОК (НЕ ПОВТОРЯЙ):
❌ Было: "Аренда стойко-места" с date_start: null
✅ Верно: "Аренда стойко-места" с date_start: "2025-01-01" (дата в преамбуле договора)
```
3. **A/B тестирование промптов (недели):** запускаем старый и новый промпт на одном документе → сравниваем результаты → выбираем лучший.
**Ключевое:** не пытаемся «обучить модель» (это не наша модель, gpt-oss-120b — API). Вместо этого:
- Улучшаем промпты
- Добавляем few-shot примеры
- Меняем подход к извлечению
- **Фиксируем все решения в БД** — чтобы через месяц понять что работало, а что нет
---
## Итоговая архитектура (сводка)
```mermaid
graph TB
subgraph "v1: Извлечение (MVP)"
U1[100+ файлов] --> F1[Фильтр мусора ⚡0ms]
F1 --> P1[Парсинг Python ⚡500ms]
P1 --> C1[Классификация LLM 🔥2-10s]
C1 --> G1[Группировка Python ⚡100ms]
G1 --> E1[Извлечение LLM 🔥30s]
E1 --> S1[(spec_current)]
end
subgraph "v2: Сверка"
CRM[CRM-выгрузка] --> AD[CRM Adapter]
S1 --> MC[Matcher ⚡хеш → fuzzy → LLM]
AD --> MC
MC --> RPT[Отчёт о расхождениях]
end
subgraph "Обратная связь"
RPT --> EXP[Эксперт]
EXP --> CORR[expert_corrections]
CORR --> PROMPT[Улучшение промптов]
PROMPT -.-> E1
end
style F1 fill:#e8f5e9
style G1 fill:#e8f5e9
style C1 fill:#fff3e0
style E1 fill:#fff3e0
style MC fill:#e3f2fd
```
---
## Практические рекомендации для DeepSeek V4 Pro
### Что кодить СЕЙЧАС (Фаза 0: изоляция)
Как описано в `opus-plan-review-2026-06-27.md`, план Opus из 6 фаз — правильный вектор. Но предлагаю **упростить Фазу 0**:
**Ф0. Предпосылки (1-2 часа):**
1. `pg_dump --schema-only` с продакшена → `schema.sql`
2. Создать новую БД `contracts_flask` на ВМ, применить `schema.sql`
3. DNS `check.kube5s.ru` → уже есть ✅
4. Пустой репо `contracts-flask` → уже есть ✅
5. `LLM_KEY` → должен быть в `.env` на ВМ
**Ф1. Бэкенд на ВМ (основная работа):**
- `~/contracts-flask/` — копия `deploy/` (db/, services/, llm_prompt.py, convert_server.py)
- ИСПРАВИТЬ `llm_prompt.py` — убрать зависимость от Lucee (`_fetch_prompt()` → `db.prompts.get_active()`)
- ИСПРАВИТЬ `services/llm.py` — взять версию из репы (свежее)
- Порт 8777, БД `contracts_flask`, systemd-юнит `contracts-flask.service`
**Ф2-Ф3. Nginx + JS:**
- Nginx: `check.kube5s.ru` → :8777
- JS: скопировать 6 файлов, поменять `VM_API='https://check.kube5s.ru'`
**Ф4+ отложить** — морда на managed Flask не нужна для v1 MVP. Используем текущий index.cfm как есть, или минимальный HTML на ВМ.
+172
View File
@@ -0,0 +1,172 @@
# Устройство ВМ contracts.kube5s.ru
> **ВАЖНО**: Это актуальная документация. Старый `architecture.md` описывает мёртвый Flask `contracts-app/site/` — к ВМ отношения не имеет.
---
## Где что лежит
```
РЕПОЗИТОРИЙ (локально) ВМ (5.172.178.213)
/home/naeel/nubes/contracts/ /home/naeel/contracts/
│ │
├── contractor/ │
│ └── deploy/ ◀─── sync.sh ───▶ ВСЁ содержимое deploy/
│ ├── app.js ├── app.js
│ ├── app_utils.js ├── app_utils.js
│ ├── compare.js ├── compare.js
│ ├── files.js ├── files.js
│ ├── groups.js ├── groups.js
│ ├── state.js ├── state.js
│ ├── tests.js ├── (нет на ВМ)
│ ├── classify_worker.py ├── classify_worker.py
│ ├── convert_doc.py ├── convert_doc.py
│ ├── convert_server.py ◀── ГЛАВНЫЙ ──▶ convert_server.py (порт 8766)
│ ├── llm_prompt.py ├── llm_prompt.py
│ ├── nginx-contracts.conf ├── nginx-contracts.conf
│ ├── db/ ├── db/
│ │ ├── __init__.py │ ├── __init__.py
│ │ ├── connection.py │ ├── connection.py
│ │ ├── contracts.py │ ├── contracts.py
│ │ ├── documents.py │ ├── documents.py
│ │ ├── prompts.py │ ├── prompts.py
│ │ ├── spec_current.py │ ├── spec_current.py
│ │ ├── spec_events.py │ ├── spec_events.py
│ │ └── supplements.py │ └── supplements.py
│ ├── services/ ├── services/
│ │ ├── __init__.py │ ├── __init__.py
│ │ ├── classify.py │ ├── classify.py
│ │ ├── grouping.py │ ├── grouping.py
│ │ ├── llm.py │ ├── llm.py
│ │ ├── parse.py │ ├── parse.py
│ │ ├── process.py │ ├── process.py
│ │ ├── unzip.py │ ├── unzip.py
│ │ └── upload.py │ └── upload.py
│ └── sync.sh │
│ ├── site/ ← Flask UI (НЕ из deploy)
│ ├── .env ← VM-специфично
│ ├── gunicorn.conf.py
│ ├── start.sh
│ ├── logs/
│ │
│ ├── classify.py ← ⛔ МУСОР (не юзается)
│ ├── grouping.py ← ⛔ МУСОР (не юзается)
│ ├── prompts.py ← ⛔ МУСОР (не юзается)
│ └── unzip.py ← ⛔ МУСОР (не юзается)
├── contracts-app/ ← ⛔ МЁРТВЫЙ Flask v1, к ВМ отношения НЕ ИМЕЕТ
├── contractor/ ← ColdFusion/Lucee (старый бекенд, не на ВМ)
└── contracts-vm/ ← ?
```
---
## ⛔ КОРНЕВЫЕ .py НА ВМ — МУСОР
На ВМ в `~/contracts/` лежат файлы:
- `classify.py`
- `grouping.py`
- `prompts.py`
- `unzip.py`
**Эти файлы НЕ ИМПОРТИРУЮТСЯ и НЕ ИСПОЛЬЗУЮТСЯ.** Они остались от старой плоской структуры.
Реально используемые версии лежат в подпапках:
- `services/classify.py`
- `services/grouping.py`
- `db/prompts.py`
- `services/unzip.py`
`convert_server.py` импортирует ТОЛЬКО из `db/` и `services/`:
```python
from db import prompts as db_prompts
from db.connection import DB_CONFIG, execute
from db import supplements as db_supplements
from db import documents as db_documents
from db import spec_current as db_spec_current
from services.upload import handle_upload
from services.unzip import handle_unzip
from services.process import run_pipeline
from llm_prompt import build_prompt
```
---
## Архитектура на ВМ
```
Браузер
Nginx :443 (nginx-contracts.conf)
├── / → 127.0.0.1:5001 (Flask, gunicorn)
│ site/app.py — отдаёт HTML (index.html, upload.html)
│ Статика: templates/, static/
└── /upload, /convert-doc, /unzip-upload,
/llm-ops, /process-v2, /parse-pdf, /static/
→ 127.0.0.1:8766 (convert_server.py)
├── db/ — PostgreSQL через psycopg2
├── services/ — бизнес-логика
└── llm_prompt.py — сборка промптов
```
### Три процесса
| Процесс | Порт | Что | Запуск |
|---|---|---|---|
| gunicorn | 5001 | Flask UI | `start.sh` |
| convert_server.py | 8766 | **Главный API-сервер** | `sync.sh` (после деплоя) |
| nginx | 443 | Прокси + SSL | systemd |
### JS-файлы — клиентские
`app.js`, `files.js`, `groups.js`, `state.js`, `compare.js`, `app_utils.js` — это **клиентский JavaScript**. Они не запускаются на ВМ как процесс. Их отдаёт Flask через HTML-шаблоны, и они выполняются в браузере.
---
## Деплой (sync.sh)
```bash
# Запускать из contractor/
bash deploy/sync.sh
```
**Сейчас деплоит только 2 файла:**
- `convert_server.py`
- `convert_doc.py`
**Должен деплоить ВСЁ из `deploy/`** (кроме `sync.sh` и `__pycache__`).
После деплоя — `pkill -f convert_server.py && nohup python3 convert_server.py &`
### Что НЕ деплоить
- `site/` — Flask UI, живёт своей жизнью
- `.env` — переменные окружения ВМ
- `gunicorn.conf.py`, `start.sh` — конфиги ВМ
- `logs/` — рантайм
---
## Как проверять соответствие
```bash
# md5 всех файлов в db/ и services/ на ВМ
ssh naeel@5.172.178.213 'md5sum ~/contracts/db/*.py ~/contracts/services/*.py'
# Сравнить с локальными
md5sum contractor/deploy/db/*.py contractor/deploy/services/*.py
```
Корневые `classify.py`, `grouping.py`, `prompts.py`, `unzip.py`**НЕ проверять**, это мусор.
---
## Версия
Актуально на 2026-06-27. При изменениях — обновлять.
@@ -0,0 +1,93 @@
# Teach flow VM smoke tests
Дата: 26.06.2026 | Проверка isolated teaching flow на VM после внедрения.
## Что проверяли
Проверялся Flask-слой VM через test client с подменой DB-слоя, чтобы подтвердить поведение новых endpoints без риска для боевой БД.
### Проверенные маршруты
- `GET /teach`
- `GET /teach/api/meta`
- `GET /teach/api/contracts`
- `GET /teach/api/context?supplement_id=...`
- `POST /teach/api/feedback`
- `GET /teach/api/feedback?contract_id=...&supplement_id=...`
## Результат smoke-test
Все маршруты вернули `200 OK` в mocked окружении.
### `/teach`
- Страница отдала HTML с заголовком `Сверка договоров — обучение`.
### `/teach/api/meta`
Вернуло дефолтные метаданные:
```json
{
"ok": true,
"prompt_version": "vm-teach-v1",
"model_name": "gpt-oss-120b"
}
```
### `/teach/api/contracts`
Вернуло список договоров и допников в ожидаемой структуре:
- `contract_id`
- `contract_number`
- `client`
- `date_signed`
- `supplements[]`
### `/teach/api/context`
Вернуло:
- `supplement`
- `current_rows`
- `previous_rows`
### `POST /teach/api/feedback`
Проверена запись feedback со значениями:
- `scope = row`
- `verdict = error`
- `error_type = wrong_price`
- `field = price`
- `llm_value` и `correct_value` как JSON
- `prompt_version = vm-teach-v1`
- `model_name = gpt-oss-120b`
- `doc_mode = amendment`
Запись успешно ушла в mock cursor и commit был вызван.
### `GET /teach/api/feedback`
Возвратил сохранённую запись в читаемом JSON виде.
## Что всплыло по окружению
Во время проверки не хватало runtime-зависимостей в VM-venv:
- `python-dotenv`
- `Flask`
- `psycopg2-binary`
- `httpx`
- `pdfplumber`
- `python-docx`
- `redis`
Эти пакеты были установлены в VM-venv, после чего smoke-test прошёл.
## Вывод
Isolated teaching flow на VM не только компилируется, но и проходит mocked smoke-test по основным endpoint'ам:
- чтение метаданных,
- загрузка списка договоров,
- загрузка контекста допника,
- запись и чтение feedback.
Старый Lucee-frontend и основной compare pipeline при этом не затрагивались.
+115
View File
@@ -0,0 +1,115 @@
# Feature: isolated teaching flow on VM
Дата: 26.06.2026 | Переход от обсуждения идеи feedback-learning к реальному isolated-flow на VM.
## Что решили
- Lucee-mordа не трогаем.
- Новая страница обучения живёт на VM по прямому URL.
- Рабочий compare pipeline не меняем.
- Feedback пишется только в отдельную таблицу `feedback`.
- Для аналитики сохраняем `prompt_version` и `model_name`.
- Значения по умолчанию берём из VM metadata endpoint / env, а не из Lucee.
## Что сделано
### 1. Изолированный Flask blueprint
Добавлен новый blueprint `teach_bp` в VM-слой:
- `/teach` — отдельная страница обучения.
- `/teach/api/meta` — дефолтные метаданные для страницы.
- `/teach/api/contracts` — список договоров и допников.
- `/teach/api/context` — текущие строки спецификации и предыдущие строки для выбранного допника.
- `/teach/api/feedback` — чтение и запись feedback.
Файлы:
- [contracts-app/site/teach.py](../../contracts-app/site/teach.py)
- [contracts-app/site/app.py](../../contracts-app/site/app.py)
### 2. Отдельная таблица feedback
Добавлена таблица `feedback` в DDL приложения. В ней хранятся:
- `contract_id`
- `supplement_id`
- `event_seq`
- `scope`
- `verdict`
- `error_type`
- `field`
- `service_name`
- `llm_value`
- `correct_value`
- `prompt_version`
- `model_name`
- `doc_mode`
- `comment`
Файл:
- [contracts-app/site/schema.py](../../contracts-app/site/schema.py)
### 3. Teach UI
Добавлена отдельная страница:
- список договоров и допников слева;
- таблица строк спецификации справа;
- кнопка `⚠` у строки для замечания;
- кнопка `✓ Всё верно`;
- кнопка `➕ Пропущена позиция`;
- отдельная форма для комментария и correct value;
- автоматический bootstrap metadata через `/teach/api/meta`.
Файл:
- [contracts-app/site/templates/teach.html](../../contracts-app/site/templates/teach.html)
### 4. Метаданные обучения
Сделан безопасный fallback:
- `prompt_version` по умолчанию: `vm-teach-v1`
- `model_name` по умолчанию: `gpt-oss-120b`
- можно переопределить через URL: `?prompt_version=...&model=...`
- можно переопределить через env:
- `TEACH_PROMPT_VERSION`
- `TEACH_MODEL_NAME`
### 5. Миграции без риска
Чтобы не ломать уже существующую БД, добавлены:
- `ALTER TABLE feedback ADD COLUMN IF NOT EXISTS prompt_version TEXT`
- `ALTER TABLE feedback ADD COLUMN IF NOT EXISTS model_name TEXT`
## Проверки
Синтаксис VM-файлов проверен через `python3 -m py_compile`:
- `app.py`
- `teach.py`
- `schema.py`
- `db.py`
- `api.py`
- `upload.py`
- `extractor.py`
- `differ.py`
- `test_routes.py`
Ошибок нет.
## Что важно не перепутать
- Старая Lucee-морда не нужна для этой фичи.
- Обучение открывается по прямому URL на VM.
- Никакого вмешательства в compare/upload pipeline нет.
- `feedback` — отдельная аналитическая шина, не часть боевого event sourcing.
## Что ещё осталось
- Подключить реальный smoke-test к VM endpoint'ам и проверить insert/select на живой БД.
- Если нужно, сделать отдельный read-only список накопленного feedback для агента.
- При желании можно later подтянуть prompt/model metadata не из URL/env, а из отдельной VM-конфигурации.
## Ключевые файлы
- [contracts-app/site/app.py](../../contracts-app/site/app.py)
- [contracts-app/site/schema.py](../../contracts-app/site/schema.py)
- [contracts-app/site/teach.py](../../contracts-app/site/teach.py)
- [contracts-app/site/templates/teach.html](../../contracts-app/site/templates/teach.html)
- [contracts-app/site/llm_client.py](../../contracts-app/site/llm_client.py)
- [contracts-app/site/extractor.py](../../contracts-app/site/extractor.py)
@@ -0,0 +1,103 @@
# Ответ Опуса — isolated training page / feedback storage
Дата: 26.06.2026 | Ответ на вопросы по отдельной странице "обучения" для проекта «Сверка договоров».
## Контекст
- Стек: Lucee/CFML 6.0, файловая маршрутизация (`teach.cfm``/teach`), PostgreSQL `baza`.
- Рабочий compare-пайплайн не трогаем: новая фича должна жить полностью изолированно.
- Цель: отдельная страница с тем же UI-скелетом, что и compare results, но с возможностью оставлять замечания по строкам таблицы операций и сохранять их в БД для дальнейшего анализа агентом.
## 1. Минимальная схема `feedback`
Для MVP достаточно плоских колонок для агрегации и `JSONB` для значений:
```sql
CREATE TABLE IF NOT EXISTS feedback (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ DEFAULT now(),
contract_id UUID,
supplement_id UUID,
event_seq INTEGER, -- NULL для missed/document
scope TEXT NOT NULL, -- 'row' | 'missed' | 'document'
verdict TEXT NOT NULL, -- 'correct' | 'error'
error_type TEXT, -- wrong_price|wrong_qty|wrong_name|wrong_date|extra_row|missed_row|wrong_action|wrong_mode
field TEXT, -- price|qty|sum|name|date_start|action|mode
service_name TEXT,
llm_value JSONB,
correct_value JSONB,
prompt_version TEXT,
doc_mode TEXT, -- partial | full_replace
comment TEXT
);
CREATE INDEX IF NOT EXISTS idx_feedback_error ON feedback(error_type);
CREATE INDEX IF NOT EXISTS idx_feedback_prompt ON feedback(prompt_version);
```
Рекомендуемый фиксированный набор `error_type`:
`wrong_price, wrong_qty, wrong_name, wrong_date, extra_row, missed_row, wrong_action, wrong_mode`.
## 2. UI первой версии vs отложить
### В MVP
- Колонка `⚠` у каждой строки таблицы операций с мини-формой: выбор `error_type` + опционально правильное значение + комментарий.
- Кнопка `[✓ всё верно]` на карточке ДС для положительного сигнала (`scope=document, verdict=correct`).
- Кнопка `[➕ пропущена позиция]` для случая, когда позиция была пропущена (`scope=missed`).
### Отложить
- Инлайн-редактирование значений прямо в ячейках.
- Статистика и дашборды на самой странице.
- Модерация замечаний, роли, удаление чужих записей.
- Разбор новых загрузок на этой странице; работать только по уже разобранным документам из БД.
## 3. Связь с исходной операцией
Правильная связь — логическая, без FK и без вмешательства в рабочий pipeline: `(contract_id, supplement_id, event_seq)`.
Почему так:
- `spec_events` уже хранит операции как `spec_events(contract_id, supplement_id, seq, action, new_values, ...)`.
- `feedback` просто повторяет эти значения как обычные поля.
- Без FK исключается риск связать feedback с жизненным циклом боевых событий или сломать вставки при повторном разборе.
Если `event_seq` когда-то переедет из-за пересборки данных, в `llm_value` / `correct_value` нужно сохранять снимок операции (`action` + `new_values`), чтобы замечание оставалось интерпретируемым.
## 4. `prompt_version`
Версию промпта хранить нужно, иначе нельзя считать регрессию между изменениями.
Но сейчас результат разбора не штампуется версией промпта, поэтому для MVP предлагается практический компромисс:
- `/teach` при сохранении замечания пишет текущую активную версию промпта из БД.
- Это честно помечается как версия на момент проверки, а не на момент исходного разбора.
- Правильная фиксация `prompt_version` в результате разбора — отдельная задача основного pipeline, не часть isolated feedback flow.
## 5. Что исключить
### PII / обезличивание
- Не хранить название/ИНН клиента, номер договора, ФИО, подписантов, реквизиты, email, телефоны.
- Не хранить оригинальный текст документа и байты файла.
### Изоляция рабочих данных
- Не использовать `ALTER` существующих таблиц.
- Не добавлять FK, триггеры или write-логики в `spec_events`, `spec_current`, `contracts`, `supplements`.
- `/teach` должен писать только в `feedback` через отдельный параметризованный endpoint.
## Итог
Опус подтвердил, что MVP должен быть:
1. Отдельной страницей с тем же UX-скелетом, что и compare results.
2. Отдельным write endpoint и отдельной таблицей `feedback`.
3. Полностью изолированным от рабочего compare-пайплайна.
4. Приспособленным для SQL-аналитики агентом через плоские колонки + `JSONB`.
Главный принцип: это не online-training, а сбор структурированного feedback для последующего анализа и планирования изменений.
@@ -0,0 +1,123 @@
# Ответ Опуса — режим «исправление ошибок / обучение»
Дата: 26.06.2025 | В ответ на обсуждение feedback-цикла.
---
## Суть
Режим: **юзер загружает документы → система выдаёт результат → юзер правит ошибки → правки сохраняются**.
Это не «обучение модели», а **накопление эталонных данных** (golden dataset) через естественный интерфейс исправления. Юзер не размечает абстрактно — он правит конкретный неверный результат.
## Что уже есть под это
- LLM возвращает поток операций: `ADD` / `UPDATE` / `DELETE` / `UNRESOLVED`
- Применяются через событийную модель (`apply_events.cfm`)
- Правка юзера = **исправленный поток операций**
- Разница «LLM выдал» vs «юзер поправил» = **чистый сигнал ошибки**
## Три уровня (не путать)
| Уровень | Что | Когда |
|---------|-----|-------|
| **Регрессия** | Правки → golden-набор → прогон промпта → % ошибок | Сразу |
| **Prompt-learning** | Анализ частых ошибок → правка промпта (few-shot/глоссарий) | Периодически |
| **Fine-tuning** | Дообучение модели на сотнях/тысячах примеров | Не сейчас |
## Чего НЕ делать
- **Автоматически вкручивать правки в промпт** — переобучение, конфликты, рост токенов
- Правильно: правки → накопитель → куратор анализирует агрегаты → batch-обновление промпта → регрессия
## Что заложить в дизайн
1. **Структурировать тип ошибки:** «пропущена строка», «неверная цена», «UPDATE вместо ADD», «ложный дубликат»
2. **Версия промпта** при каждом результате — чтобы знать актуальность ошибки
3. **Конфиденциальность** — реальные договоры с реквизитами копятся в БД, обсудить с заказчиком
---
**Решение:** отложить. Позже вернуться и спроектировать.
---
# Часть 2 — Конкретный UI для отметки ошибок
Дата: 26.06.2025 | Опус изучил реальный вывод (карточка + таблица операций) и предложил привязку к элементам интерфейса.
## Привязка: не к абзацам, а к строкам таблицы операций
Результат — **таблица операций** (`Действие / Услуга / Цена / Кол-во / Сумма / Дата`), а не текст-простыня. Замечание цепляется к строке таблицы, а не к абзацу исходника.
### Как выглядит (на реальном примере)
```
✓ допник-1-XXX002-01200_3.docx — 2 оп., partial (14с) [✓ всё верно]
+2 ~0 -0
Действие Услуга Цена Кол-во Сумма Дата ⚠
ADD WAF: Positive Technologies, в составе… 104021.67 1 104021.67 2026-03-30 [⚠]
ADD Облачный диск Valo Cloud, в составе… 68700 1 68700 2026-02-01 [⚠]
[ ➕ Система пропустила позицию ]
```
### Клик по `[⚠]` → мини-форма под строкой
```
Тип ошибки: ( ) неверная цена/сумма
( ) неверное кол-во
( ) неверное наименование услуги
(•) лишняя строка — этой операции быть не должно
( ) неверное действие (должно быть UPDATE/DELETE, а не ADD)
( ) неверная дата
Правильное значение: [_____________] (необязательно)
Комментарий: [_____________] (необязательно)
[ Сохранить ]
```
### `[✓ всё верно]` — вверху карточки
Один клик = положительный сигнал, без расписывания.
### `[➕ Система пропустила позицию]` — под таблицей
Для случая, когда услуга была в документе, но LLM её не извлекла. Открывает форму ввода пропущенной строки.
## Что уходит в базу (обезличенно)
```
operation: ADD
service: "Облачный диск Valo Cloud, в составе…" ← без названия клиента
field: price
llm_value: 68700
correct: 68000
error_type: wrong_price
prompt_version: v1.0.178
```
Никаких названий компаний, ФИО, № договора — только структура услуги и числа.
## Почему так, а не поле у каждого абзаца
- Реальный вывод — **таблица**, а не текст. Абзацев нет, есть операции
- Привязка к операции даёт агрегацию по типу ошибки
- Готовая дельта «LLM выдала X → правильно Y» как обучающий сигнал
- Ровная укладка в событийную модель (`apply_events.cfm`)
---
**Решение:** отложить. Ждать «делай» для реализации.
---
# Часть 3 — Текст для заказчика
**Этап опытной эксплуатации (обучение системы)**
На первое время предлагаем режим проверки: вы загружаете реальные документы, система выдаёт результат, а вы отмечаете ошибки — что распознано неверно (пропущена строка, неверная цена, неправильное сопоставление и т.п.).
Эти отметки **в обезличенном виде** (без названий компаний, ФИО, реквизитов и иных персональных данных) накапливаются в базе и используются для дальнейшей настройки и обучения системы.
Это позволит откалибровать сервис на ваших реальных договорах, а не на тестовых примерах, и системно повышать точность.
@@ -0,0 +1,515 @@
# Полные ответы Опуса — изолированная страница /teach (feedback + обучение)
Дата: 26.06.2026 | Сохранено post-factum из чат-сессии.
---
# ЧАСТЬ 1: Ответ про few-shot и эталонный датасет
Хороший вопрос. Дай я сначала посмотрю, как сейчас устроены эти «6 файлов» и формирование промпта, чтобы мнение было по делу.
Searching codebase for "формирование промпта для LLM на основе эталонных файлов few-shot examples"
Searched for regex `6 (файл|file|example|эталон|reference)`, 1 result
Searched for regex `few.?shot|эталон|example|пример|reference|опорн` (`**/contracts-app/**/*.py`), no results
Searched for files matching `**/llm_prompt.py`, 1 match
Searched for files matching `**/prompt*.{py,cfm}`, 2 matches
Searched for regex `opus_cases|out_100files|6 файл|six`, 2 results
Read file:///home/naeel/nubes/contracts/contractor/deploy/llm_prompt.py#1-1, lines 1 to 120
Read file:///home/naeel/nubes/contracts/testgen/opus_cases
Посмотрел, как сейчас устроено: промпт в llm_prompt.py — это хардкод-инструкция с доменным глоссарием и парой ручных few-shot примеров (`ПРИМЕР 1`, `ПРИМЕР 2`), а «6 файлов» — это по сути синтетические кейсы из opus_cases для проверки.
Моё мнение по фиче.
## Идея здравая, но есть важная развилка
Реальные проверенные документы — это очень ценно. Синтетика (gen_opus.py) не воспроизводит грязь реальных договоров: кривые таблицы, объединённые ячейки, «мощность в составе названия», нестандартные формулировки ДС, опечатки в суммах. На 6 искусственных файлах легко получить иллюзию качества, которая рассыпется на первом же настоящем пакете.
Но ключевой вопрос: **что именно строить из архива?** Тут смешаны две разные вещи.
## 1. Эталонный датасет (input → проверенный output) — да, делать
Самое полезное. Но важно: «загрузить архив документов» недостаточно. Чтобы это работало, на каждый документ нужен **проверенный правильный результат** (итоговая спецификация / список ops). Без эталонного ответа это просто куча PDF — не на чем калибровать.
То есть фича должна быть не «загрузи архив», а «загрузи архив + зафиксируй проверенную спецификацию как ground-truth». Тогда получаем regression-набор: прогнал промпт → сравнил с эталоном → видишь реальный процент ошибок.
## 2. Авто-генерация промпта из этих файлов — осторожно
Полностью «формировать промпт» из загруженных файлов я бы не делал. Риски:
- **Раздувание контекста** — если пихать реальные договоры целиком как few-shot, промпт пухнет, растёт цена/латентность, и модель начинает копировать формат конкретного образца.
- **Переобучение под layout** — закладываешь форматы 3-х клиентов, ломается на четвёртом.
- **Конфиденциальность** — реальные договоры с реквизитами/ценами уезжают в LLM на каждом запросе. Это надо явно проговорить с заказчиком.
Лучше из архива **извлекать паттерны, а не сам текст**: новые единицы измерения, варианты названий услуг, типы формулировок ДС («изложить в новой редакции» и т.п.) — и обогащать ими глоссарий и правила в промпте. 1-2 самых показательных кейса можно добавить как few-shot, остальное — в eval-набор.
## Что я бы предложил по факту
- **Разделить два контура**: (а) `golden dataset` из реальных пар вход→проверенный_выход для регрессии; (б) сам промпт — оставить компактным, обогащать его выводами из анализа датасета, а не сырыми файлами.
- Фича «загрузка архива» → ведёт в **разметку/верификацию** (человек подтверждает правильный результат), а не сразу в промпт.
- На этом наборе гонять метрику (precision/recall по ops), и менять промпт только если метрика растёт.
Резюме: направление правильное — уходить от 6 синтетических файлов надо. Но ценность не в «архиве документов», а в **архиве проверенных пар документ↔результат**. И этим калибровать/тестировать, а не механически генерировать промпт.
Это обсуждение — код не трогаю. Скажешь «делай» по конкретному пункту — приступлю.
---
# ЧАСТЬ 2: Ответ про «типа обучение» (feedback-learning цикл)
Да, это сильнее предыдущего варианта — и вот почему, плюс где подвох.
## Почему это лучше «загрузки архива»
Главная проблема прошлого варианта была: откуда взять «проверенный правильный ответ». Здесь она решается сама собой — **интерфейс исправления ошибок и есть инструмент разметки**. Юзер не размечает абстрактно, а правит конкретный результат системы. На выходе автоматически копится то самое: пара «вход → проверенный выход». Это правильный способ построить эталонный датасет.
И что важно — у вас уже есть подходящая структура под это. Результат LLM — это поток операций (`ADD` / `UPDATE` / `DELETE` / `UNRESOLVED`) в llm_prompt.py, а применяются они через событийную модель (apply_events.cfm). Значит правка юзера — это **исправленный поток операций**, а разница «что выдала LLM» vs «как поправил юзер» = чистый сигнал ошибки. Это удобно: не свободный текст «тут неверно», а конкретная дельта.
## Но «типа обучения» — это важная оговорка
Само по себе это **не обучение модели**. Цикл «учится» только если правки где-то сохраняются и переиспользуются. Если правка живёт в рамках одной сессии и исчезает — система не умнеет, это просто ручная корректировка. Так что ключевое в фиче — не сам акт правки, а **накопление и переиспользование** правок.
Дальше есть три уровня, и не надо их путать:
1. **Регрессия** (самое дешёвое и полезное сразу) — правки копятся как golden-набор, на нём гоняется промпт, видно реальный % ошибок.
2. **Prompt-learning** — из частых ошибок осознанно дополняется глоссарий/правила/few-shot в промпте. Это и есть практичное «обучение» в вашем случае.
3. **Fine-tuning модели** — реальное дообучение. Дорого, нужен объём (сотни-тысячи примеров), отдельная инфраструктура. На старте не нужно.
## Где подвох — не вкручивать правки в промпт автоматически
Соблазн: «каждую правку юзера сразу добавлять в промпт». Так делать нельзя:
- **переобучение** под конкретные форматы;
- **рост контекста** и цены;
- **конфликтующие правки** от разных юзеров → промпт деградирует.
Правильнее: правки идут в накопитель → периодически человек/куратор смотрит агрегат → осознанно обновляет промпт → прогоняет регрессию. Batch, а не автоматом на каждый клик.
## Что бы я ещё заложил в дизайн
- **Структурировать тип ошибки**, а не свободный текст: «пропущена строка» / «неверная цена» / «надо было UPDATE, а не ADD» / «ложный дубликат вместо UNRESOLVED». Тогда правки можно агрегировать и видеть системные слабости промпта, а не разбирать вручную.
- Фиксировать **версию промпта**, на которой получен результат — иначе через месяц непонятно, актуальна ли ещё ошибка.
- Учесть **конфиденциальность**: реальные договоры с реквизитами/ценами копятся в БД — проговорить с заказчиком.
Резюме: режим «загрузил → система выдала → юзер поправил → правка сохранилась» — да, делать стоит, это естественный сборщик эталонных данных и ловушка системных ошибок. Только «обучение» держать осознанным (batch-обновление промпта + регрессия), а не авто-инъекцией каждой правки.
Код не трогаю — это обсуждение. Скажешь «делай» — распишу конкретную реализацию.
---
# ЧАСТЬ 3: Короткий вариант для заказчика
Вот короткий вариант для заказчика:
---
**Этап опытной эксплуатации (обучение системы)**
На первое время предлагаем режим проверки: вы загружаете реальные документы, система выдаёт результат, а вы отмечаете ошибки — что распознано неверно (пропущена строка, неверная цена, неправильное сопоставление и т.п.).
Эти отметки **в обезличенном виде** (без названий компаний, ФИО, реквизитов и иных персональных данных) накапливаются в базе и используются для дальнейшей настройки и обучения системы.
Это позволит откалибровать сервис на ваших реальных договорах, а не на тестовых примерах, и системно повышать точность.
---
Если нужно ещё короче (одним абзацем) или, наоборот, добавить пункт про согласие на обработку данных — скажи, поправлю.
---
# ЧАСТЬ 4: Ответ про привязку замечаний к строкам таблицы
Дай гляну, как сейчас выглядит вывод результата — чтобы предложить привязку замечаний к реальным элементам интерфейса, а не абстрактно.
Read file:///home/naeel/nubes/contracts/contractor/deploy/services
Read memory file:///home/naeel/.vscode-server/data/User/workspaceStorage/dc98ff3e658364e1826ee6585ebe4b5e/GitHub.copilot-chat/memory-tool/memories/repo/contracts-two-panels.md
Searched for regex `target_id|r1|new_row|action.*ADD|операц|spec|строк` (`**/contractor/view.cfm`), no results
Searched for files matching `**/contracts-app/site/templates/**`, no matches
Не «у каждого абзаца» — это привязка не к тому объекту. Результат работы системы — это не текст-простыня, а **таблица спецификации**: набор строк-услуг, каждая получена операцией LLM (`ADD`/`UPDATE`/`DELETE`/`UNRESOLVED`) и имеет свой id (`r1`, `r2`…). Замечание логично цеплять **к строке результата**, а не к абзацу исходника.
## Как я бы это сделал
**Уровень 1 — отметка на строке (основное).**
У каждой строки итоговой таблицы — маленькая иконка «⚠ ошибка». Клик открывает мини-форму с **выбором типа ошибки** (не свободный текст):
- неверная цена / объём / сумма
- неверное наименование
- лишняя строка (надо было удалить)
- пропущена строка (есть в документе — нет в результате)
- неверное сопоставление (надо UPDATE существующей, а не новая ADD; или наоборот)
- неверный тип ДС / режим (partial vs новая редакция)
Плюс необязательное поле «комментарий» свободным текстом.
**Уровень 2 — отметка «пропущено».**
Случай «строки вообще нет в результате» не покрывается иконкой на строке. Нужна отдельная кнопка «➕ Система пропустила позицию» над/под таблицей.
**Уровень 3 — общая оценка (опционально).**
Одна кнопка вверху: «Результат в целом верный / есть ошибки». Дёшево, но даёт быстрый сигнал по проценту удачных разборов.
## Почему так, а не поле у абзаца
- **Структурированный фидбек агрегируется.** «Тип ошибки = неверное сопоставление встречается в 40% ДС» → понятно, что чинить в промпте. Свободный текст у абзацев придётся разбирать вручную, аналитики ноль.
- **Замечание = дельта операции.** Юзер фактически правит строку → разница «что выдала LLM» vs «как должно быть» и есть обучающий сигнал. Это ложится на вашу событийную модель (apply_events.cfm) почти без натяжки.
- **Меньше шума.** Поле ввода под каждым абзацем визуально перегружает и провоцирует расплывчатые комментарии.
## Что копится в базе (обезличенно)
На каждое замечание:
- тип ошибки + (опц.) комментарий
- что выдала система по этой строке
- как поправил юзер (правильное значение)
- версия промпта
- **без** названий компаний/ФИО/реквизитов — только структура и числа
## Как это сформулировать заказчику (1 фраза)
> В режиме проверки у каждой строки результата будет отметка «ошибка» с выбором типа (неверная цена, пропущена позиция, неверное сопоставление и т.п.). Эти отметки в обезличенном виде копятся в базе и используются для настройки точности.
Хочешь — распишу конкретную реализацию (поля в БД, эндпоинт сохранения, изменения в шаблоне вывода). Код пока не трогаю, жду «делай».
---
# ЧАСТЬ 5: Ответ про UI с примером на допник-1-XXX002
Отлично, теперь предметно. Твой вывод — это карточка документа + **таблица операций** (`Действие / Услуга / Цена / Кол-во / Сумма / Дата`). Замечания цепляем именно к строкам этой таблицы. Вот как это выглядело бы на твоём примере:
```
✓ допник-1-XXX002-01200_3.docx — 2 оп., partial (14с) [✓ всё верно]
+2 ~0 -0
Действие Услуга Цена Кол-во Сумма Дата ⚠
ADD WAF: Positive Technologies, в составе… 104021.67 1 104021.67 2026-03-30 [⚠]
ADD Облачный диск Valo Cloud, в составе… 68700 1 68700 2026-02-01 [⚠]
[ ➕ Система пропустила позицию ]
```
**Клик по `[⚠]` в строке** → раскрывается мини-форма прямо под строкой:
```
Тип ошибки: ( ) неверная цена/сумма
( ) неверное кол-во
( ) неверное наименование услуги
(•) лишняя строка — этой операции быть не должно
( ) неверное действие (должно быть UPDATE/DELETE, а не ADD)
( ) неверная дата
Правильное значение: [_____________] (необязательно)
Комментарий: [_____________] (необязательно)
[ Сохранить ]
```
**Кнопка `[✓ всё верно]`** вверху карточки — если разбор документа корректен целиком. Один клик = положительный сигнал, ничего расписывать не надо.
**Кнопка `➕ Система пропустила позицию`** — для случая, которого нет в таблице (услуга была в документе, но LLM её не извлекла). Открывает форму ввода пропущенной строки.
## Что уходит в базу по одному замечанию (обезличенно)
На примере второй строки, если юзер укажет «неверная цена, правильно 68000»:
```
operation: ADD
service: "Облачный диск Valo Cloud, в составе…" ← наименование услуги, без названия клиента
field: price
llm_value: 68700
correct: 68000
error_type: wrong_price
prompt_version: v1.0.178
```
Никаких «ЗАО ХХХ002», № договора, ФИО — только структура услуги и числа.
## Почему так, а не поле под каждым абзацем
Твой реальный вывод подтверждает: это **таблица**, а не текст. Поле у «абзаца» некуда вешать — абзацев тут нет, есть операции. А привязка к операции даёт:
- агрегацию по типу ошибки (видно, что чинить в промпте),
- готовую дельту «LLM выдала X → правильно Y» как обучающий сигнал,
- ровную укладку в вашу событийную модель (apply_events.cfm).
Хочешь — распишу конкретику реализации (колонка в таблице вывода, эндпоинт сохранения, структура таблицы в БД). Код не трогаю, жду «делай».
---
# ЧАСТЬ 6: ДЕТАЛЬНЫЙ ПЛАН — страница /teach
Это план, не правка кода — изучу структуру, чтобы план лёг на реальный код, а не в воздух. Читаю ключевые файлы.
Read file:///home/naeel/nubes/contracts/contractor/db.cfc
Read file:///home/naeel/nubes/contracts/contractor/Application.cfc
Read memory file:///home/naeel/.vscode-server/data/User/workspaceStorage/dc98ff3e658364e1826ee6585ebe4b5e/GitHub.copilot-chat/memory-tool/memories/repo/contracts-deploy.md
Read memory file:///home/naeel/.vscode-server/data/User/workspaceStorage/dc98ff3e658364e1826ee6585ebe4b5e/GitHub.copilot-chat/memory-tool/memories/repo/contracts-lucee-test.md
Изучил структуру. Lucee-приложение с файловой маршрутизацией (`teach.cfm``/teach`), PostgreSQL `baza`, есть событийная модель (`spec_events`, `spec_current`) и уже готовый read-only SQL-эндпоинт `/db/query?sql=…` в db.cfc — через него агент и будет читать БД. Вот детальный план.
---
# План: страница `/teach` (опросник + обучение)
## 1. Принцип изоляции — что НЕ трогаем
Маршрутизация в Lucee файловая → **новый путь = новый файл**, существующий код не редактируется вообще:
| Не трогаем | Добавляем |
|---|---|
| `api.cfm`, `process*.cfm`, `apply_events.cfm`, db.cfc, Application.cfc, `upload.cfm`, `view.cfm` | `teach.cfm`, `teach_api.cfm`, `teach_schema.cfm` |
| существующие таблицы (`spec_events`, `contracts`…) | **одна новая таблица** `feedback` |
Никаких `ALTER` существующих таблиц, никаких правок в `db.cfc/schema()`. Только чтение чужих таблиц + запись в свою.
## 2. Новые файлы (3 шт.)
1. **`teach_schema.cfm`** — одноразовый: `CREATE TABLE IF NOT EXISTS feedback (…)`. Зашёл по URL один раз → таблица создана.
2. **`teach.cfm`** — сама страница:
- список уже разобранных договоров/ДС (читает `contracts` + `supplements` + `spec_events`);
- выбрал ДС → рисует ту же таблицу операций (`Действие/Услуга/Цена/Кол-во/Сумма/Дата`), что в твоём примере, но с колонкой `⚠` и кнопками `[✓ всё верно]`, `[➕ пропущена позиция]`;
- JS отправляет замечание `POST`-ом на `teach_api.cfm`.
3. **`teach_api.cfm`** — приём замечания: **параметризованный** `INSERT` в `feedback` (никакого конкатенированного SQL — защита от инъекций). Возвращает JSON `{ok:true}`.
> Источник данных для таблицы операций — `spec_events` (`action`, `new_values` JSONB, `comment`, `seq`). При реализации сверим, что карточка «2 оп., partial» строится именно отсюда.
## 3. Схема таблицы `feedback` (ядро — продумано под анализ агентом)
Главные поля вынесены отдельными колонками (не в JSON), чтобы агент агрегировал простым SQL; значения — в JSONB.
```sql
CREATE TABLE IF NOT EXISTS feedback (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ DEFAULT now(),
-- привязка (внутренняя трассировка, в обучающий экспорт НЕ идёт)
contract_id UUID, -- FK-логически на contracts, без жёсткого constraint
supplement_id UUID,
event_seq INTEGER, -- какая операция ДС (NULL для "пропущено"/"документ в целом")
-- уровень и вердикт
scope TEXT NOT NULL, -- 'row' | 'missed' | 'document'
verdict TEXT, -- 'correct' | 'error'
-- суть ошибки (для агрегации)
error_type TEXT, -- wrong_price|wrong_qty|wrong_name|wrong_date|
-- extra_row|missed_row|wrong_action|wrong_mode
field TEXT, -- price|qty|sum|name|date_start|action|mode
-- обучающий сигнал: что выдала система vs как правильно
service_name TEXT, -- наименование услуги (обезличено)
llm_value JSONB, -- что выдала LLM
correct_value JSONB, -- что указал юзер
-- контекст результата
prompt_version TEXT, -- версия промпта на момент разбора
doc_mode TEXT, -- partial | full_replace
comment TEXT, -- свободный комментарий юзера (необяз.)
reviewer TEXT -- обезличенный id сессии/юзера (необяз.)
);
CREATE INDEX IF NOT EXISTS idx_feedback_error ON feedback(error_type);
CREATE INDEX IF NOT EXISTS idx_feedback_prompt ON feedback(prompt_version);
```
## 4. Что пишется по каждому типу замечания
| Действие юзера | scope | verdict | error_type | llm_value → correct_value |
|---|---|---|---|---|
| `[✓ всё верно]` на карточке | `document` | `correct` | — | — |
| `⚠` на строке: неверная цена | `row` | `error` | `wrong_price` | `{"price":68700}``{"price":68000}` |
| `⚠`: лишняя строка | `row` | `error` | `extra_row` | вся операция → `null` |
| `⚠`: должно быть UPDATE, а не ADD | `row` | `error` | `wrong_action` | `{"action":"ADD"}``{"action":"UPDATE"}` |
| `[➕ пропущена позиция]` | `missed` | `error` | `missed_row` | `null``{name,price,qty,sum,date}` |
| неверный режим ДС | `document` | `error` | `wrong_mode` | `{"mode":"partial"}``{"mode":"full_replace"}` |
Каждая запись самодостаточна: видно **что было** и **как надо** → готовая обучающая пара.
## 5. Обезличивание
- В `feedback` **не копируем** название клиента, № договора, ФИО, реквизиты.
- Храним только: `service_name` (тип услуги — «WAF Positive Technologies»), числа, тип ошибки, версию промпта.
- `contract_id/supplement_id` — это UUID (не имя), для внутренней трассировки. В **обучающий экспорт** агента эти id не включаются — только структурные поля.
- Для заказчика формулировка: *«в обезличенном виде, без названий компаний и персональных данных»* — соответствует фактической схеме.
## 6. Как агент читает и анализирует (потом)
Через уже существующий `/db/query?sql=…` (read-only) или напрямую psql. Примеры:
**Системные слабости промпта:**
```sql
SELECT error_type, field, count(*) AS n
FROM feedback WHERE verdict='error'
GROUP BY error_type, field ORDER BY n DESC;
```
**Качество по версии промпта (регрессия):**
```sql
SELECT prompt_version,
count(*) FILTER (WHERE verdict='correct') AS ok,
count(*) FILTER (WHERE verdict='error') AS err
FROM feedback GROUP BY prompt_version;
```
**Выгрузка обучающих пар (для правки промпта/few-shot):**
```sql
SELECT service_name, error_type, llm_value, correct_value
FROM feedback WHERE verdict='error' AND scope='row';
```
Дальше агент: смотрит агрегат → предлагает правку промпта/глоссария → прогоняет на накопленных парах → сравнивает метрику до/после. **Никакой авто-инъекции** правок в промпт — только осознанное batch-обновление.
## 7. Версия промпта — нюанс
Сейчас результат разбора **не штампуется** версией промпта. Варианты:
- (минимум, без правки основного кода) `/teach` пишет в `prompt_version` **текущую активную** версию промпта на момент замечания — приблизительно, с оговоркой;
- (правильно, отдельной задачей позже) при разборе сохранять `prompt_version` в результат — но это уже касается основного пайплайна, делать отдельно и по «делай».
Флажок: на старте берём активную версию, точность привязки уточним позже.
## 8. Этапы внедрения
1. `teach_schema.cfm` → создать таблицу `feedback`.
2. `teach.cfm` (чтение разобранных ДС + рендер таблицы с `⚠`) — без сохранения, только UI.
3. `teach_api.cfm` (параметризованный INSERT) + подключить JS-отправку.
4. Проверка end-to-end на одном реальном ДС (как твой `допник-1-XXX002`).
5. 2–3 контрольных SQL-запроса агента → убедиться, что данные читаемы и агрегируются.
6. (позже, отдельно) первый цикл анализа → правка промпта → регрессия.
## 9. Открытые вопросы к тебе
1. **`/teach` работает только по уже разобранным** документам (из БД) — или нужна и загрузка-разбор прямо на странице? (загрузка-разбор = переиспользование основного пайплайна, сложнее).
2. **Доступ к `/teach`** — нужен ли пароль/ограничение, или dev-стенд открыт?
3. **`service_name`** — оставляем как есть (тип услуги) или дополнительно маскируем?
4. **Кто reviewer** — нужно различать разных проверяющих или достаточно анонимной сессии?
---
Это план, код не трогал. Скажешь по каким пунктам «делай» (и ответишь на вопросы из §9) — начну с `teach_schema.cfm`.
---
# ЧАСТЬ 7: Ответ про промпт для Опуса
Текст для Опуса хороший — чёткие требования и правильные ограничения (не трогать пайплайн, без online-training, обезличенность). Но в нынешнем виде Опус ответит **обобщённо**, потому что в промпте нет контекста вашего стека и текущей схемы. По каждому из 5 вопросов это критично. Что я бы добавил, прежде чем отправлять:
## Чего не хватает Опусу для предметного ответа
1. **Стек и маршрутизация.** Lucee/CFML 6.0, файловая маршрутизация (`teach.cfm``/teach`), PostgreSQL `baza`. Без этого Опус не поймёт, как «не трогать пайплайн» технически (а ответ — просто новый файл, без правок существующих).
2. **Текущая схема — особенно для Q3.** У вас уже есть событийная модель:
- `spec_events(contract_id, supplement_id, seq, action, target_hash, new_values JSONB, comment, status)`
- `spec_current`, `contracts`, `supplements`.
Связь с исходной операцией = `(contract_id, supplement_id, event_seq)`, и это **уже отвечает на Q3**: ничего в пайплайне трогать не надо, фидбек просто ссылается на `seq` логически, без FK-constraint. Опус должен это знать, иначе предложит схему, не совпадающую с вашей.
3. **Форма результата (для Q1/Q2).** Таблица операций `Действие / Услуга / Цена / Кол-во / Сумма / Дата`, режимы `partial | full_replace`. Дайте Опусу реальный пример вывода (твой `допник-1-XXX002`).
4. **Ключевой факт для Q4.** Сейчас результат разбора **не штампуется версией промпта** — этого в промпте нет, а это определяет весь ответ на Q4. Без упоминания Опус посоветует «просто сохраняйте prompt_version», не зная, что её сейчас негде взять, кроме «текущей активной».
5. **Как агент потом читает БД (для Q1).** Есть read-only эндпоинт `/db/query?sql=…` в db.cfc. Это аргумент за «плоские колонки `error_type/field/prompt_version` + значения в JSONB» — чтобы агрегировалось обычным SQL.
## Мелочи по формулировкам
- Q5 стоит разнести: отдельно **PII** (имена/реквизиты/№ договора — не хранить) и отдельно **изоляция рабочих данных** (никаких `ALTER`/FK на боевые таблицы, только своя `feedback`). Сейчас они слиты в один вопрос.
- Добавь явно: *«предложи минимальный набор `error_type` как enum-значения»* — иначе Опус может уйти в свободный текст ошибок, который потом не агрегируется.
---
Хочешь — могу либо (а) переписать твой промпт для Опуса с этим контекстом, либо (б) сам ответить на все 5 вопросов по вашей реальной схеме (у меня он есть). Скажи, что полезнее.
---
# ЧАСТЬ 8: Финальный предметный ответ по 5 вопросам
Понял, отвечаю по существу — по вашей реальной схеме.
## 1. Минимальная схема `feedback` для MVP
Плоские колонки для агрегации + JSONB для значений:
```sql
CREATE TABLE IF NOT EXISTS feedback (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ DEFAULT now(),
-- связь с исходной операцией (логическая, без FK)
contract_id UUID,
supplement_id UUID,
event_seq INTEGER, -- NULL для missed/document
scope TEXT NOT NULL, -- 'row' | 'missed' | 'document'
verdict TEXT NOT NULL, -- 'correct' | 'error'
error_type TEXT, -- enum-значения, см. ниже
field TEXT, -- price|qty|sum|name|date_start|action|mode
service_name TEXT, -- наименование услуги (обезличено)
llm_value JSONB, -- что выдала система
correct_value JSONB, -- как правильно
prompt_version TEXT,
doc_mode TEXT, -- partial | full_replace
comment TEXT
);
CREATE INDEX IF NOT EXISTS idx_feedback_error ON feedback(error_type);
CREATE INDEX IF NOT EXISTS idx_feedback_prompt ON feedback(prompt_version);
```
Фиксированный набор `error_type` (enum как CHECK или просто документированный список):
`wrong_price, wrong_qty, wrong_name, wrong_date, extra_row, missed_row, wrong_action, wrong_mode`.
Всё. Ни истории версий записи, ни статусов модерации в MVP не нужно.
## 2. UI первой версии vs отложить
**В MVP:**
- колонка `⚠` у каждой строки таблицы операций → мини-форма: выбор `error_type` (радио) + опц. «правильное значение» + опц. комментарий;
- кнопка `[✓ всё верно]` на карточке ДС (дешёвый положительный сигнал, `scope=document, verdict=correct`);
- кнопка `[➕ пропущена позиция]` (`scope=missed`).
**Отложить:**
- инлайн-редактирование значений прямо в ячейке (пока хватает «правильное значение» в форме);
- дашборд статистики на самой странице (агрегация — задача агента через SQL, не UI);
- модерация/удаление чужих замечаний, роли, авторизация по пользователям;
- разбор-на-лету (загрузка нового документа) — работаем по уже разобранным из БД.
## 3. Связь с исходной операцией без вмешательства в pipeline
Ключ — **логическая ссылка `(contract_id, supplement_id, event_seq)`**, БЕЗ внешнего ключа на `spec_events`. У вас операции уже лежат в `spec_events(contract_id, supplement_id, seq, action, new_values…)``seq` стабилен в рамках договора. `feedback` просто хранит эти три значения как обычные поля.
Почему без FK:
- FK на `spec_events` = риск, что повторный разбор/перезапись событий уронит вставку фидбека или каскадом заденет данные — это и есть «вмешательство». Логическая ссылка развязывает таблицы полностью.
- `feedback` — отдельная таблица, ничего в боевых не меняем (никаких `ALTER`, никаких триггеров).
Если `seq` теоретически может переехать при reset/reparse — дополнительно дублируем в `llm_value` снимок операции (`action` + `new_values`), чтобы замечание оставалось интерпретируемым даже без живого `spec_events`. Этого достаточно.
## 4. prompt_version — хранить сразу, но честно
Хранить **да** — без неё нельзя мерить регрессию (стало лучше/хуже после правки промпта). Но: сейчас результат разбора версией промпта **не штампуется**, взять её из результата негде.
Поэтому для MVP, не трогая pipeline:
- `/teach` на момент сохранения замечания читает **текущую активную** версию промпта из БД (ту, что Lucee отдаёт как активную) и пишет в `prompt_version`.
- Это приближение: оно верно, если промпт не менялся между разбором и проверкой. Помечаем это как «версия на момент проверки», не «на момент разбора».
Правильная привязка (штамповать `prompt_version` в результат при разборе) — отдельная задача по основному пайплайну, делается позже и по явному решению, не в этом изолированном flow.
## 5. Что обязательно исключить
**PII / обезличивание — не хранить:**
- название/ИНН клиента, № договора, ФИО, подписантов, реквизиты, email/телефоны;
- оригинальный текст документа и байты файла.
Храним только тип услуги (`service_name`), числа, тип ошибки, версию промпта. `contract_id/supplement_id` — это UUID (не имена), для внутренней трассировки; в обучающий **экспорт** агента они не попадают.
**Изоляция рабочих данных — исключить из дизайна:**
- любые `ALTER` существующих таблиц, FK-constraint'ы на боевые таблицы, триггеры;
- запись в `spec_events / spec_current / contracts / supplements``/teach` только читает их, пишет исключительно в `feedback`;
- никакого generic-SQL write-эндпоинта на публичной странице — только параметризованный INSERT в `teach_api.cfm` (защита от инъекций).
---
Это ответ-проектирование, код не трогаю. Скажешь «делай» — начну с `teach_schema.cfm` (создание таблицы), затем `teach.cfm` и `teach_api.cfm`.
@@ -0,0 +1,151 @@
# Анализ ответа Опуса — zip_source, UI-режимы, промпты
Дата: 26.06.2025 | Ответ на [opus-request-zip-plan.md](../Files/opus-request-zip-plan.md)
---
## 1. Оценка ответа в целом
**Качество: высокое.** Опус прочитал реальный код, разобрался в архитектуре, дал конкретные диффы по слоям. Не «размышления вообще», а точные строки и функции. 95% рекомендаций — правильные.
**Что упущено:**
- Lucee-слой (`upload.cfm`, `api.cfm`) — тоже участвует в upload, но Опус его не проанализировал
- `confidence` — предлагает сохранять в БД, но не говорит где именно брать (LLM возвращает? парсить из промпта?)
- Порог 100 файлов для «Потока» — спорный, обсудим ниже
---
## 2. По пунктам
### 2.1. `zip_source` — ✅ СОГЛАСЕН полностью
План по слоям правильный. Ключевые моменты:
- **`zip_source` не участвует в classify/group/compare** — верно. Чисто визуальный атрибут.
- **Формат: имя ZIP с расширением** — да, `«Ромашка.zip»`.
- **PK не трогаем (UUID)**, «ID = zip/filename» только для отображения — верно.
- **Дедупликация по паре `(zip_source, name)`** — ⚠️ самый критичный момент. Опус прав: если не сменить ключ, одноимённые файлы из разных ZIP будут перезаписываться. Но **надо проверить**: текущий код в `addRegularFile()` (files.js) ищет по `f.name`. При добавлении `zip_source` нужно либо:
- Ключ = `zip_source + "/" + filename` (как предлагает заказчик)
- Или ключ = `(zip_source || "") + filename`
Я за вариант с конкатенацией в одну строку — проще для сравнения.
- **unzip.py не трогаем** — верно. Имя ZIP уже есть на фронте (`file.name`).
### 2.2. Вариант отображения — ✅ СОГЛАСЕН (Вариант А)
Заголовок-секция ZIP + отступ `padding-left: 24px`.
- Просто, без нового состояния
- Соответствует тому что описал заказчик
- Опциональное сворачивание — да, но не в первой итерации
### 2.3. Два UI-режима — ⚠️ ЧАСТИЧНО СОГЛАСЕН
**Плюсы:**
- Бэкенд не меняется — правильно
- `state.ui.mode` — хорошее место
- Сводный отчёт («зелёное сворачиваем, красное показываем») — отличная идея
- Авто-определение + ручной override — разумно
**Спорные моменты:**
- **Порог 100 файлов** — слишком низкий для автоматического предложения. При 100 файлах текущий UI работает нормально (скролл, 50vh). Реальный болевой порог — **200-300+**. Предлагаю порог **200**.
- **«Поток» сейчас не нужен.** Если заказчик работает с 5-50 файлами, весь Stream Mode — оверинжиниринг. Но архитектурно заложить `state.ui.mode` — дёшево и правильно.
### 2.4. Промпты — ✅ СОГЛАСЕН, с уточнениями
#### Classify — проблемы А-Д:
**А. Counterparty / блок про стороны НУБЕС** — 🔴 КРИТИЧНАЯ.
Опус абсолютно прав. Промпт сейчас не говорит что НУБЕС = Исполнитель. LLM возвращает случайную сторону. Чинится одной вставкой в промпт. Делать **первым**.
НО: Опус предлагает «ИНН 7727... (взять у заказчика)». Это перебор. Достаточно:
```
НУБЕС известен как: «НУБЕС», «ООО НУБЕС», «ООО "НУБЕС"», «Nubes».
counterparty — ВСЕГДА вторая сторона, НИКОГДА не НУБЕС.
```
**Б. parent_number у contract** — ✅ верно. `parent_number = null` для contract.
**В. Мусорные документы** — ✅ верно. Добавить примеры в `doc_type=other`.
**Г. Few-shot примеры в classify** — ✅ верно. 1-2 примера улучшат точность.
**Д. confidence** — ⚠️ спорно. Опус говорит «добавить колонку и показывать low в отчёте». Но `confidence` сейчас даже не сохраняется. Предлагаю **сначала убрать из промпта** (меньше путаницы), а потом, когда будет реальная потребность — добавить и колонку, и парсинг.
#### Compare (diff) — ✅ СОГЛАСЕН
- «UNRESOLVED вместо дубль-ADD при сомнении» — верно
- `temperature=0.1` для diff — проверить (скорее всего уже)
- `full_replace` → автоматическое удаление старых строк на стороне Python — **умная идея**, снижает нагрузку на LLM
#### Разные промпты под сценарии — ✅ СОГЛАСЕН
Не нужно. Один classify + один diff. Меньше рассинхрона.
### 2.5. Гомоглифы — ⚠️ ОСТОРОЖНО
Опус предлагает:
- `С/C → C` (латиница)
- `О/0` — «трактовать осторожно»
- `Ё → Е`
**Моё мнение:**
- `С→C` и `Ё→Е`**опасно**. Это меняет семантику номера. `МЭС-123``МЭC-123`. Лучше: **не заменять, а добавить второй проход сравнения** — если точное совпадение не найдено, попробовать с гомоглифами. Или нормализовать ОБА варианта (и кириллицу, и латиницу) к единому представлению, но сохранять оригинал для отображения.
- Конкретно для `Ё`: да, `Ё→Е` допустимо (в делопроизводстве Ё часто заменяют на Е). Но лучше сделать настраиваемым.
### 2.6. Приоритеты — ✅ СОГЛАСЕН с корректировкой
| Что | Приоритет Опуса | Моя оценка |
|-----|----------------|-----------|
| Counterparty в промпте | P0 | P0 ✅ |
| doc_type=other мусор | P0 | P0 ✅ |
| Гомоглифы | P0 | P1 ⚠️ (осторожно, не ломать) |
| zip_source | P1 | P1 ✅ |
| Группировка по ZIP в UI | P1 | P1 ✅ |
| Режим «Поток» | P2 | P3 (отложить, нет потребности) |
| confidence в БД | P3 | P3 (или убрать из промпта) |
| diff-UNRESOLVED | P3 | P2 (дёшево, большой эффект) |
---
## 3. Что Опус упустил
### 3.1. Lucee-слой
`upload.cfm` и `api.cfm` на Lucee тоже обрабатывают загрузку. Если файл идёт через Lucee (а не напрямую на VM), `zip_source` нужно прокинуть и там. Надо проверить — идёт ли upload через Lucee или напрямую на VM.
**Факт:** судя по `index.cfm`, JS грузится с VM (`contracts.kube5s.ru/static/app.js`), а upload идёт на `VM_API + '/upload'`. Значит Lucee в upload **не участвует**. `zip_source` в Lucee не нужен. Опус оказался прав молча.
### 3.2. Промпты в БД vs хардкод
Опус верно заметил: «промпты берутся из БД (Lucee), fallback — хардкод. Менять надо в БД через интерфейс промптов». Это **критично важно для исполнителя**: если просто поправить `FALLBACK_EXTRACT`/`FALLBACK_DIFF` в `llm_prompt.py` — в проде ничего не изменится, потому что используется версия из БД.
**Порядок правки промптов:**
1. Сначала в БД через UI (`/prompt.cfm`)
2. Потом в хардкоде (для fallback)
### 3.3. Порог для «Потока»
100 файлов — слишком консервативно. Таблица с `max-height: 50vh` и `overflow-y: auto` нормально работает при 100-150 файлах. Предлагаю **200** как порог для автопредложения. Но лучше — **сделать настраиваемым** (константа в начале app.js).
---
## 4. Итоговое мнение
**Ответ Опуса — хороший план.** 95% рекомендаций принимаю.
**Что делаем прямо сейчас (P0):**
1. Правка classify-промпта: блок про стороны НУБЕС + parent_number=null + примеры мусора
2. Правка diff-промпта: UNRESOLVED вместо дубль-ADD, temperature проверка
**Что делаем дальше (P1):**
3. `zip_source` сквозь все слои (БД → Python → JS)
4. Группировка по ZIP в таблице (Вариант А)
5. Дедупликация по `(zip_source, filename)`
**Что откладываем:**
6. Режим «Поток» — пока нет потребности
7. Гомоглифы — нужно больше примеров от заказчика
8. confidence — убрать из промпта, вернуть когда будет нужно
**Главный риск (ещё раз):** дедупликация в `addRegularFile()`. Без правки ключа на `(zip_source, name)` — фича сломается на первом же случае одинаковых имён в разных ZIP.
@@ -0,0 +1,133 @@
# Рецензия DeepSeek V4 Pro на отчёт Opus
Дата: 27.06.2026
---
## Общая оценка: ⭐⭐⭐⭐⭐ (5/5)
Opus сделал ТО, ЧТО НУЖНО: прочитал код, нашёл расхождения с документацией, не писал код, не рисовал графы. Это эталонный анализ.
---
## Что Opus нашёл такого, чего я НЕ заметил
### 🔴 КРИТИЧЕСКОЕ: PyPDF2 теряет структуру таблиц
Я читал `parse.py`, видел `PyPDF2`, но **не осознал** что он извлекает текст построчно и не сохраняет колонки. Для спецификаций — это катастрофа. Таблица «наименование | цена | кол-во | сумма | дата» после PyPDF2 станет плоским текстом, и LLM придётся угадывать где какая колонка.
**Opus прав на 100%.** Это вероятно главный риск точности прямо сейчас.
Надо: `pdfplumber` (для цифровых PDF) или `camelot`.
### 🔴 КРИТИЧЕСКОЕ: Кода сверки CRM↔фискальная НЕТ
Я знал это из ответов заказчика, но Opus проверил grep'ом (`crm|фискал|fiscal|сверк|PAYG|reconcil`) — нашёл только «акт сверки» как тип мусора. Это greenfield. Меняет планирование: не «эволюция», а «новый модуль».
### 🟡 ВАЖНОЕ: service-description.md устарел
Я дал Opus читать `service-description.md` как «актуальный». Opus обнаружил что он описывает парсинг через Lucee (Java POI/PDFBox), а **реальный код** (`upload.py``parse.py`) парсит на ВМ через PyPDF2/python-docx. Без Java. Без Lucee.
**Моя ошибка:** я не перепроверил service-description.md на соответствие коду. Надо поправить доку.
### 🟡 ВАЖНОЕ: llm_prompt.py зависит от Lucee
`build_prompt()` (основной пайплайн) ходит HTTP в Lucee за активным промптом. А `build_classify_prompt()` берёт из БД напрямую. Непоследовательно + точка отказа.
### 🟡 ВАЖНОЕ: chat.cfm содержит захардкоженный API-ключ
Плюс ссылается на `spec_rows` вместо `spec_current` — сломан вдвойне.
### 🟢 Безопасность: verify=False в llm.py
TLS-проверка отключена. Надо включить.
---
## Где Opus прав, а где нет
| Тезис Opus | Моё мнение |
|------------|------------|
| **Гипотеза 1 (гибрид): ⚠ частично** — «текущий код — чистый pipeline, и это правильно» | ✅ **Согласен.** 131 тест на детерминированные функции — сильный аргумент против агентов. |
| **Гипотеза 2 (фильтр мусора): ⚠ частично** — «этап 1 по имени файла ненадёжен» | ⚠ **Частично.** Для имён типа «счет-фактура №123 от 01.01.2025.docx» regex надёжен. Но для «scan001.pdf» — да, бесполезен. Вывод: этап 1 — приоритизация, не жёсткий дроп. |
| **Гипотеза 3 (парсинг на ВМ): ❌ отвергнута** | ✅ **Полностью согласен.** Моя гипотеза была основана на устаревшей документации. Парсинг уже на ВМ. |
| **Гипотеза 4 (трёхуровневый matching): ⚠ частично** — «difflib опасен на коротких токенах» | ✅ **Согласен.** Примеры «IPv4↔IPv6», «10 кВт↔15 кВт» убедительны. Матчить по структурным атрибутам, не по сырой строке. |
| **Гипотеза 5 (MVP): ⚠ частично** — «нужен CRM-прогон уже в v1» | ✅ **Согласен.** Иначе v1 будет «готов», а к цели не приблизимся. |
| **Гипотеза 6 (PG-очередь): ✅ подтверждена** | ✅ **Согласен.** Плюс Opus добавил: сейчас очереди нет вообще, in-process ThreadPool. |
| **«LLM бесплатный ≠ быстрый»** — не наращивать проходы | ✅ **Согласен.** 5-30s × 100+ файлов = узкое место. |
| **«Не отказываться от детерминированного matching»** | ✅ **Согласен.** Воспроизводимость и аудит важнее «бесплатности». |
### Единственное где я НЕ согласен:
**Opus говорит «не использовать имя файла для фильтрации».**
Я считаю: regex `(сч[её]т|акт|плат[её]ж|УПД|сверк|инвойс|invoice)` по имени файла — ДОСТАТОЧНО надёжен для пред-фильтрации. «Счет-фактура №123.docx» — это всегда мусор. Да, «scan001.pdf» не отфильтруется — но он уйдёт на этап 2 (первые 2KB текста). Риск ложного срабатывания на договоре с именем «счет-фактура» — ноль.
---
## Что Opus упустил
1. **testgen/** — в репо есть `testgen/generate.py`, `testgen/pools.py`, `testgen/templates.py`. Это генератор синтетических тестовых данных. Opus предлагает «тестировать матчинг на синтетике», но не знает что инструмент уже есть.
2. **convert_server.py роутинг** — Opus не проверил ВСЕ эндпоинты. Например, `/api/batch-progress`, `/api/apply-groups`, `/api/cleanup`, `/api/sync` — они есть, но не проанализированы.
3. **services/llm.py — расхождение ВМ↔репо** — Opus заметил `verify=False`, но не предложил КАК выяснить какая версия каноническая (репо или ВМ). Я поднимал этот вопрос в `opus-plan-review-2026-06-27.md`.
4. **LibreOffice — узкое место для .doc**`/convert-doc` запускает LibreOffice subprocess последовательно с таймаутом 30с. На пачке старых .doc файлов это потенциально медленнее даже LLM. Но на практике .doc — редкость (заказчик говорил про docx/pdf).
5. **OCR не нужен** — заказчик уточнил: «только текст, сканов не будет». Значит риск «потребуется внешний OCR-сервис» снимается.
---
## Дополнения из второго анализа (подтверждения)
Второй анализ полностью подтвердил выводы Opus. Дополнительно акцентировано:
- **Дрейф документации** — не только `service-description.md`, но и другие .md могут устареть. Правило: **код > документация**. Всегда проверять.
- **base64 в БД** — `upload.py` кладёт файлы как base64 в `documents`. На 100+ многомегабайтных файлах это раздует БД. Риск масштабирования.
- **Нет устойчивой очереди** — `classify.py` = `ThreadPoolExecutor` в одном процессе. Падение процесса = потеря прогресса. PG-очередь добавит durability.
---
## Итог: что я беру в работу
### Немедленно (следующий чат, по команде «делай»):
1. **Заменить PyPDF2 на pdfplumber** в `services/parse.py` — критично для точности
2. **Починить chat.cfm** — убрать ключ, `spec_rows``spec_current`
3. **Включить verify=True** в `services/llm.py`
4. **Отвязать llm_prompt.py от Lucee**`build_prompt``db.prompts.get_active()`
5. **Поправить service-description.md** — парсинг на ВМ, не через Lucee
### Вторая очередь:
6. **Детерминированная проверка `sum == price*qty`** — бесплатный сигнал ошибок
7. **Золотой набор** — 30-50 реальных документов с ручной разметкой
8. **Метрики** — логировать `_safe_json_parse` срабатывания, UNRESOLVED, латентность
### Третья очередь (после золотого набора):
9. **Спроектировать нейтральную CanonicalRow** для CRM↔фискальная
10. **Прототип матчинга** на синтетике из testgen/
---
## Вывод
Opus дал **отличный анализ**. Главная ценность — нашёл расхождения кода и документации, которые я пропустил. PyPDF2 — критическая находка. То что кода CRM-сверки нет — меняет приоритеты: не «эволюция», а «новый модуль».
**Что отсеялось после двух анализов:**
- OCR не нужен (только текст, без сканов)
- LLM не использовать агрессивнее (бесплатный ≠ быстрый, 5-30s/вызов)
- Агенты не нужны (131 тест на pipeline, детерминизм > гибкость)
- RabbitMQ не нужен (PG-очередь достаточна)
**Что осталось в работе (5 немедленных + 3 второй очереди):**
1. pdfplumber вместо PyPDF2
2. chat.cfm: ключ + таблица
3. llm.py: verify=True
4. llm_prompt.py: отвязать от Lucee
5. service-description.md: поправить
6. Проверка sum==price*qty
7. Золотой набор 30-50 документов
8. Метрики (_safe_json_parse, UNRESOLVED, латентность)
Следующий шаг: команда «делай» → правлю пункты 1-5.
@@ -0,0 +1,209 @@
# Задание Opus: Архитектурное исследование — Сверка договоров v2
Дата: 27.06.2026 | От: DeepSeek V4 Pro (через Крупского) | Кому: Opus
---
## ⛔ ЧЕГО НЕ ДЕЛАТЬ
- **НЕ пиши код.** Ни строчки Python, SQL, JS, ничего. Только концепции.
- **НЕ рисуй mermaid-диаграммы.** Только текст. Исполнитель (DeepSeek) сам нарисует если надо.
- **НЕ предлагай «переписать с нуля».** Продакшен надо эволюционировать.
- **НЕ лезь в файлы за пределами списка ниже.** Экономия токенов.
## 📋 ЗАЧЕМ ЭТОТ АНАЛИЗ
Ты — исследователь. Твой отчёт пойдёт **DeepSeek V4 Pro** (мне). Я буду по нему ПИСАТЬ КОД.
У нас нет промежуточных результатов — неизвестна точность LLM на реальных 100+ документах,
неизвестны типичные расхождения CRM↔фискальная. Нужна архитектура, которую можно
итеративно улучшать по мере данных.
Главное — **концептуальные решения**. Не реализации.
---
## ⛔ КАКИЕ ФАЙЛЫ СМОТРЕТЬ
### Код (18 + 18 + 7 = 43 файла):
```
contractor/index.cfm — HTML/CSS скелет (v1.0.178)
contractor/upload.cfm — приём файлов (form/JSON/multipart/iframe)
contractor/chunk.cfm — чанковая загрузка
contractor/process.cfm — SSE-пайплайн v1 (прямой LLM)
contractor/process_v2.cfm — SSE-пайплайн v2 (→ ВМ, event sourcing)
contractor/apply_events.cfm — ADD/UPDATE/DELETE → spec_current
contractor/extractor.cfm — старый вариант извлечения spec_rows
contractor/parser.cfm — парсинг docx/pdf (PDFBox + POI)
contractor/differ.cfm — сравнение допников с базовым договором
contractor/chat.cfm — Q&A (⚠ сломан: spec_rows вместо spec_current)
contractor/api.cfm — создание схемы БД
contractor/Application.cfc — конфигурация Lucee + datasource
contractor/db.cfc — REST-обёртка БД
contractor/view.cfm — просмотр текста документа
contractor/prompt.cfm — версионирование промптов
contractor/reset_contract.cfm — сброс контракта
contractor/test.cfm — тест
contractor/jars.cfm — проверка Java-библиотек
contractor/deploy/convert_server.py — HTTP-роутер (:8766)
contractor/deploy/llm_prompt.py — промпты + fallback (⚠ зависит от Lucee)
contractor/deploy/classify_worker.py — фоновый процесс (subprocess)
contractor/deploy/convert_doc.py — парсинг docx/pdf
contractor/deploy/services/upload.py — загрузка
contractor/deploy/services/unzip.py — ZIP
contractor/deploy/services/classify.py — классификация (LLM: тип/номер/контрагент)
contractor/deploy/services/process.py — пайплайн сравнения
contractor/deploy/services/llm.py — вызов LLM API (⚠ ВМ↔репо расходятся)
contractor/deploy/services/parse.py — PDF (PyPDF2) / DOCX (python-docx)
contractor/deploy/services/grouping.py — группировка (гибрид LLM→Python)
contractor/deploy/db/connection.py — psycopg2
contractor/deploy/db/documents.py — CRUD документов
contractor/deploy/db/supplements.py — CRUD дополнений
contractor/deploy/db/spec_current.py — CRUD текущей спецификации
contractor/deploy/db/spec_events.py — event sourcing (seq, reset)
contractor/deploy/db/contracts.py — CRUD контрактов
contractor/deploy/db/prompts.py — CRUD промптов (seed defaults)
contractor/deploy/app.js — фронтенд (VM_API, render, stepper)
contractor/deploy/app_utils.js — утилиты
contractor/deploy/state.js — центральный state
contractor/deploy/files.js — загрузка/рендер файлов
contractor/deploy/groups.js — карточки групп
contractor/deploy/compare.js — SSE-сравнение
contractor/deploy/tests.js — 131 юнит-тест
```
### Документы (6 штук):
```
History/topics/customer-qa-2026-06-26.md — ответы заказчика (ОБЯЗАТЕЛЬНО)
History/architecture/vm-layout.md — ⭐ раскладка ВМ (самый актуальный)
History/architecture/service-description.md — описание сервиса
History/architecture/two-panel-architecture.md — две панели просмотра
History/opus-plan-review-2026-06-27.md — разбор предыдущего плана Opus
History/llm-analysis/decoupling-final-plan.md — план размоноличивания JS
```
### ⛔ НЕ ЧИТАТЬ:
```
History/architecture/architecture.md — ⛔ МЁРТВЫЙ Flask contracts-app/site/
History/architecture/block-diagram.md — ⛔ МЁРТВЫЙ Flask
History/architecture/pipeline.md — ⛔ МЁРТВЫЙ Flask
sim/ testgen/ dogovora/ FILES/ Files/ DOC/ contracts-flask/ contracts-vm/
README.md sonnet-v2-request.md
History/sessions/ History/features/ History/llm-analysis/*
(КРОМЕ decoupling-final-plan.md)
```
---
## 📋 КОНТЕКСТ: что уже надумал DeepSeek
Я (DeepSeek) уже провёл предварительный анализ. Ниже — мои гипотезы.
**Твоя задача: подтвердить, опровергнуть или дополнить.** Не просто соглашаться.
### Гипотеза 1: Гибрид pipeline + агент
Pipeline для 95% стандартных случаев (дёшево, предсказуемо).
Лёгкий оркестратор-агент — только для краевых случаев (нестандартный формат, ошибка парсинга).
Pure agents — дорого: 100 файлов × 500 токенов оркестратора = 50K токенов только на планирование.
### Гипотеза 2: Трёхэтапный фильтр мусора
Этап 1 (0 токенов): regex по имени файла — «счёт», «акт», «платёж» → сразу garbage.
Этап 2 (0 токенов): ключевые слова в первых 2KB текста — «СЧЕТ-ФАКТУРА», «АКТ СВЕРКИ» → garbage.
Этап 3 (LLM): только оставшиеся → точный тип (contract/supplement/specification).
Экономия: ~50% LLM-вызовов при 50% мусора в пачке.
### Гипотеза 3: Перенос парсинга на ВМ
Сейчас: JS → ВМ → Lucee (Java PDFBox/POI) → обратно на ВМ.
Предложение: JS → ВМ → pdfplumber + python-docx (без Java, без Lucee).
Убирает latency сетевого вызова, упрощает деплой.
### Гипотеза 4: Трёхуровневый matching CRM↔фискальная
Уровень 1 (0 токенов): хеш нормализованного названия.
Уровень 2 (0 токенов): fuzzy string matching (difflib).
Уровень 3 (LLM): только для 10-20% несопоставленных.
80% строк матчатся без LLM.
### Гипотеза 5: MVP-границы
v1: извлечение спецификаций из договоров (без CRM-сверки). Доказать что LLM вообще справляется.
v2: сверка CRM↔фискальная (когда качество извлечения подтверждено).
v3: красивый UI, чат, автообучение на коррекциях.
### Гипотеза 6: PostgreSQL-очередь вместо RabbitMQ
Для 100+ файлов пару раз в неделю — хватит `documents.classify_status='pending'` + `FOR UPDATE SKIP LOCKED`. RabbitMQ — оверкилл.
---
## ❓ ВОПРОСЫ К OPUS (5 блоков)
### Блок 1: Насколько агрессивно использовать LLM?
**Контекст:** LLM заказчика — gpt-oss-120b через api.aillm.ru — **БЕСПЛАТНЫЙ**. Никаких затрат на токены.
Сейчас LLM используется 3 раза за прогон:
- Классификация (тип/номер/контрагент)
- Извлечение спецификации
- Сравнение ДС
**Вопросы:**
1.1. Имеет ли смысл использовать LLM **больше** раз на одном документе? Например:
- Два прохода извлечения (второй — верификация первого)?
- LLM-as-judge: проверять свой же результат и исправлять ошибки?
- LLM для принятия решений по ходу пайплайна (оркестратор)?
1.2. Где LLM **реально** добавляет ценность, а где детерминированный код справится лучше?
(С учётом что LLM медленный: 2-30 секунд на вызов.)
1.3. Если LLM бесплатный — может **вообще отказаться от детерминированного matching** (Гипотеза 4) и всё гонять через LLM? Плюсы/минусы.
### Блок 2: Архитектура — pipeline или агент?
2.1. Критика гибридного подхода (Гипотеза 1). Что я упустил? В каких сценариях чистый pipeline или чистые агенты были бы лучше?
2.2. Если гибрид — где конкретно проходят границы? Какие решения принимает оркестратор, какие — pipeline?
2.3. Есть ли смысл в нескольких специализированных агентах (parse-agent, classify-agent, compare-agent) вместо одного оркестратора? Плюсы/минусы.
### Блок 3: Обработка 100+ файлов и фильтрация
3.1. Критика трёхэтапного фильтра (Гипотеза 2). Какие документы он пропустит? Ложные срабатывания?
3.2. Парсинг на ВМ (Гипотеза 3) — правильно ли отказываться от Java PDFBox/POI в пользу pdfplumber/python-docx? Качество парсинга таблиц упадёт или вырастет?
3.3. Какие ещё узкие места будут при 100+ файлах, кроме классификации? Память? Сеть? Диск?
### Блок 4: Сверка CRM ↔ фискальная
4.1. Критика трёхуровневого matching (Гипотеза 4). В каких случаях fuzzy matching (difflib) даст ложные срабатывания? Примеры.
4.2. PAYG (суффикс `-m`, нет артикулов): как сопоставлять? Достаточно убирать `-m` и сравнивать названия, или нужна более сложная логика?
4.3. Если заказчик НЕ даст формат CRM в ближайшее время — как спроектировать модуль чтобы не простаивать? Что можно сделать уже сейчас (без CRM-данных)?
### Блок 5: Итеративность и метрики
5.1. Критика MVP-границ (Гипотеза 5). Может что-то из v2 нужно уже в v1? Или наоборот — что-то из v1 отложить?
5.2. Какие **конкретные метрики** внедрить с первого дня? Не «точность» вообще, а что именно измерять? Как измерять без эталонных данных?
5.3. Цикл обратной связи: заказчик проверяет → исправляет → система учитывает. Как это делать без дообучения модели (gpt-oss-120b — API, не наша)? Только few-shot в промпте? Или есть другие подходы?
5.4. **Главный вопрос:** мы не знаем точность LLM. С чего начать? Просто прогнать 100 реальных документов через текущий код и посмотреть? Или сначала улучшить фильтрацию/промпты, а потом прогонять?
---
## 📐 ФОРМАТ ОТВЕТА
1. **Executive Summary** — 3-5 абзацев, главные выводы
2. **По каждой гипотезе** — вердикт (✅ подтверждаю / ⚠ частично / ❌ отвергаю) + аргументация
3. **Ответы на вопросы** — по блокам, кратко
4. **Что я упустил** — твои собственные идеи, которых нет в моих гипотезах
5. **MVP-план** — что делать в ближайшие 2 недели (пункты, не код)
6. **Риски** — что может пойти не так
## ⚠️ ОГРАНИЧЕНИЯ
- Конфиденциальность: никаких S3, только локал / файловая шара
- Managed Lucee: не можем ставить pip-пакеты на нём
- ВМ: 5.172.178.213, Ubuntu, Python 3.12, PostgreSQL 15
- LLM: gpt-oss-120b, 8000 токенов, ~5-30s на вызов, БЕСПЛАТНО
+100
View File
@@ -0,0 +1,100 @@
# Ответ Опуса — фоновая классификация при 50+ файлах
Ответ на `History/opus-classify-async-question.md` от 25.06.2026.
---
## Диагноз: что рвётся на самом деле
**nginx НЕ виноват.** `classify-batch` отдаёт `202` мгновенно. `batch-progress` — короткие независимые запросы, укладываются в 30с. Классификация идёт `classify_worker → api.aillm.ru` напрямую, минуя nginx.
Реальное узкое место — **симулятор сдаётся на 4-й минуте** (120 итераций × 2с), а 69 файлов при 4 воркерах не успевают: 69 / 4 ≈ 18 волн × 10-15с/LLM-вызов = 3-5 минут > лимита симулятора.
---
## Ответы
### Q1. `subprocess.Popen` для прода — норм?
Для масштаба сотни-тысячи файлов регулярно `Popen`-на-запрос **неадекватен**:
- Нет супервизии: упал worker — никто не узнал (stdout/stderr в DEVNULL)
- Нет авто-рестарта
- Гонки при параллельных батчах с одним `batch_id`
**Целевая архитектура:** постоянный worker-сервис под отдельным systemd-юнитом + очередь задач на основе БД.
Почему БД-очередь, а не Redis:
- Work items уже в БД (`documents.classify_status='pending'`)
- Устойчиво к рестартам ВМ — resumable
- Redis не установлен в проде
- Для одной ВМ Celery/RQ — оверкилл
Дополнительно при тысячах файлов:
- retry/backoff на 429/5xx от api.aillm.ru
- rate-limit к LLM API
- Аккуратное повышение параллелизма (не 4 воркера, а 10-15)
### Q2. nginx при долгих запросах
nginx НЕ узкое место. classify-batch=202, batch-progress<30s, классификация идёт worker→LLM напрямую. Реальное узкое: throughput (4 воркера) + лимит симулятора. Прогресс durable в БД, UI не зависит от дедлайна.
### Q3. Два classify подряд — два Popen
- **Разные `batch_id`** — безопасно. Каждый процесс работает только над своими документами.
- **Один и тот же `batch_id` дважды** — гонка! Оба сделают `reset_classify_status` + повторные LLM-вызовы → двойные траты токенов, неконсистентные счётчики.
**Решение:** НЕ убивать предыдущий процесс (опасно — может испортить БД). Guard через проверку: если для `batch_id` уже есть running-процесс → вернуть `{"ok": false, "error": "already running"}`. Реализация: либо файл-лок (`/tmp/classify_<batch_id>.lock`), либо запись в БД.
### Q4. Мониторинг/логирование
DEVNULL недопустим на масштабе — падения невидимы.
**Минимум сейчас:** лог-файл per batch `/home/naeel/contracts/logs/classify_<batch_id>.log` (Python logging) вместо DEVNULL.
**Целевое:** job-таблица в БД:
```sql
CREATE TABLE classify_jobs (
batch_id UUID PRIMARY KEY,
started_at TIMESTAMP,
finished_at TIMESTAMP,
total INT, done INT, failed INT,
error_text TEXT,
pid INT
);
```
Даёт: честный прогресс, видимость падений, per-doc retry, аудит.
---
## Целевая архитектура (будущее)
```
systemd: contracts.service (HTTP)
systemd: classify-worker.service (фоновая классификация)
flow:
HTTP → 202 + запись в classify_jobs (status='queued')
classify-worker (постоянно):
SELECT batch_id FROM classify_jobs WHERE status='queued' LIMIT 1
→ status='running'
→ classify_batch(batch_id)
→ status='done' + метрики
→ следующий батч
Прогресс: documents.classify_status (pending/classified/failed)
Поллинг: GET /api/batch-progress?batch=X → count by status
```
Преимущества:
- Устойчиво к рестартам (состояние в БД)
- Один процесс обрабатывает батчи последовательно — нет гонок
- systemd мониторит и рестартует при падении
- Логи systemd/journald
---
## Минимум сейчас (без переписывания архитектуры)
1. **Лог-файл вместо DEVNULL:** `stdout=open(log_path, 'w')`
2. **Лимит симулятора:** увеличить `MAX_WAIT` до 600с (10 мин) для bulk-тестов
3. **Guard от двойного classify:** файл-лок `/tmp/classify_<batch_id>.lock` — если существует, вернуть "already running"
+28
View File
@@ -0,0 +1,28 @@
# Вопрос к Опусу — фоновая классификация при 50+ файлах
## Что сделано
`POST /api/classify-batch` получает `{batch_id}`, запускает `classify_worker.py`
через `subprocess.Popen` и сразу возвращает `202 {total: N}`.
Фронтенд поллит `/api/batch-progress` каждые 2с пока `done >= total`.
`classify_worker.py` — отдельный питон-процесс, импортирует `services/classify.py`,
у которого внутри `ThreadPoolExecutor(max_workers=4)` для параллельных
LLM-вызовов (один файл = один LLM-запрос к api.aillm.ru).
## Проблема
С 4 файлами полный цикл работает. С 69 файлами:
- HTTP-сервер жив, `/health` отвечает
- `classify_worker` работает
- Симулятор с внешней машины не дожидается конца — на 5+ минутах рвётся сеть/nginx
## Вопросы
1. Архитектурно `subprocess.Popen` норм для прода? Или что-то более надёжное (очередь, systemd-таймер, воркер-пул)?
2. Что делать с nginx при долгих запросах? `batch-progress` — короткий поллинг, он не должен рваться. Но сам classify через фронтенд не идёт — только через воркер. Где узкое место?
3. При двух быстрых классификациях подряд — второй `Popen` создаст второй процесс. Старый ещё не умер. Надо проверять и убивать предыдущий? Или пусть оба работают (разные batch_id)?
4. Как правильно мониторить/логировать фоновый процесс? Сейчас stdout/stderr в `/dev/null`.
+87
View File
@@ -0,0 +1,87 @@
# Разбор плана Opus от 2026-06-27
## Что произошло
Opus'у дали задачу: «изучи код и напиши подробный план для DeepSeek V4 Pro по созданию параллельного Flask-стека 1:1 с Lucee↔ВМ, домен check.kube5s.ru».
Opus изучил репозиторий (без доступа к ВМ), считая `contractor/deploy/` зеркалом продакшена, и выдал план из 6 фаз.
---
## Что Opus выяснил правильно
### Архитектура продакшена
- **Lucee — почти пустая морда.** Только `index.cfm` (HTML+CSS-скелет), `chat.cfm` (Q&A), `Application.cfc`. Вся логика — на ВМ.
- **JS-файлы** (app.js, files.js, groups.js, state.js, compare.js, app_utils.js) — клиентские, загружаются через `<script src>` из `/static/` на ВМ.
- **Бэкенд на ВМ** — `convert_server.py` на порту 8766, импортирует `services/*.py` и `db/*.py`.
- **Nginx** маршрутизирует: `/` → Flask :5001 (UI), `/upload`, `/convert-doc`, `/process-v2` и т.д. → Python :8766 (API), `/lucee/` → managed Lucee.
### Проблемы, которые Opus обнаружил
- **`chat.cfm` сломан** — ссылается на таблицу `spec_rows`, которой нет (правильная — `spec_current`).
- **`llm_prompt.py` зависит от Lucee** — `_fetch_prompt()` ходит в `contractor.luceek8s.../prompt.cfm`. Для изолированного стека надо переключить на `db.prompts.get_active()`.
- **Нет `schema.sql`** — прод-БД `baza` создавалась руками. Для новой БД нужен `pg_dump --schema-only`.
- **Рестарт через nohup**, а не systemd (хотя память говорит про `contracts.service`).
### План Opus (6 фаз)
| Фаза | Что |
|---|---|
| Ф0 | Предпосылки: DNS, дамп схемы БД, managed-домен морды, LLM_KEY |
| Ф1 | Бэкенд на ВМ: `~/contracts-flask/`, порт 8777, БД `contracts_flask`, systemd, разрыв связи с Lucee |
| Ф2 | Nginx + SSL: новый server-блок `check.kube5s.ru` → :8777, certbot |
| Ф3 | JS-фронтенд: копии 6 JS-файлов, `VM_API='https://check.kube5s.ru'`, раздача через `/static/` |
| Ф4 | Морда на managed Flask: очистить `contracts-app`, портировать `index.cfm` в Jinja2, Dockerfile |
| Ф5 | Чат: перенести на ВМ как `POST /chat` (Python), контекст из `spec_current` |
| Ф6 | Проверка изоляции и сквозной тест |
---
## Что Opus НЕ знал (выяснили позже)
### 1. `contractor/deploy/` — ПОБАЙТОВОЕ зеркало ВМ
Opus предполагал это, но не знал точно. **Мы проверили md5 всех файлов** — ВСЕ файлы в `db/` и `services/` совпадают с ВМ (кроме `services/llm.py`, который отличается). Корневые `classify.py`, `grouping.py`, `prompts.py`, `unzip.py` на ВМ — мусор, не используются.
### 2. `contracts-app` — отдельный git-репозиторий
Opus предлагал «очистить contracts-app». Мы его случайно удалили вместе с `.git`. Репо: `https://gitea.services.ngcloud.ru/Nail/contracts-app`. Потом Наиль сказал «в ПИЗДУ его» и дал новый пустой репо: `https://gitea.services.ngcloud.ru/Nail/contracts-flask.git`.
### 3. `check.kube5s.ru` — DNS есть, nginx нет
DNS указывает на 5.172.178.213, но nginx не знает про этот домен → проваливается в default-сервер (obdai.ru), показывает чужой LLM-UI.
### 4. Рестарт — и nohup, и systemd
На ВМ реально перезапуск идёт через `pkill -f convert_server.py; nohup python3 ... &` (из sync.sh). Systemd-сервис `contracts.service` упоминается в памяти, но неясно, активен ли он.
### 5. JS-файлы уже на ВМ
6 JS-файлов лежат в `~/contracts/` (корень) и раздаются бэкендом через `/static/`. Для нового стека их надо скопировать в `~/contracts-flask/` и поменять `VM_API`.
---
## Моё мнение о плане Opus
### Что хорошо
- **Полная изоляция** — новая папка, новый порт, новая БД, новый systemd-юнит. Прод не заденешь.
- **Разрыв связи с Lucee** — правильно замечено, критично.
- **schema.sql через pg_dump** — правильно, ручное воссоздание схемы гарантирует ошибки.
- **6 фаз с зависимостями** — логичный порядок.
### Что плохо / упущено
1. **Ф4 (Flask-морда) завязана на CI/CD managed-платформы**, которую Opus не знает. План деплоя Flask-морды — «Dockerfile + gitea CI/CD» — слишком общий. Нужны конкретные шаги под Nubes managed Python.
2. **Не учтён `services/llm.py`** — единственный файл, который реально расходится между ВМ и репой. При копировании бэкенда надо брать версию из репы (она новее?) или с ВМ — надо выяснить.
3. **JS-файлы** — Opus предлагает копировать, но не уточняет что `VM_API` зашит в `app.js` (строка 3). Это единственное место, которое надо менять.
4. **Чат (Ф5)** — предлагает перенести на ВМ, но `chat.cfm` в Lucee всё равно сломан. Имеет смысл сделать чат сразу правильно, а не портировать сломанное.
5. **Нет упоминания `tests.js`** — на ВМ этого файла нет, но в репе есть. Надо включить в новый стек.
### Вердикт
План **рабочий, но требует уточнений** по пунктам выше. Главная ценность — Opus правильно понял архитектуру и предложил изоляцию. Детали (managed-деплой, `llm.py`, чат) надо доработать.
---
## Текущее состояние (на 2026-06-27)
- ✅ DNS `check.kube5s.ru` → 5.172.178.213
- ✅ Пустой репо `contracts-flask` (`.gitignore` создан)
-`contractor/deploy/` подтверждён как зеркало ВМ
- ✅ Мусорные файлы на ВМ помечены
- ✅ Документация `vm-layout.md` создана
- ❌ Nginx для `check.kube5s.ru` не настроен
- ❌ Нет дампа схемы БД
- ❌ Не выбран managed-домен для Flask-морды
- ❌ Не выяснено, какая версия `services/llm.py` правильная (ВМ или репа)
+285
View File
@@ -0,0 +1,285 @@
# Ответ Опуса — 30 прицельных тестовых кейсов
Ответ на `History/opus-testcases-request.md` от 25.06.2026.
Опус изучил реальный код пайплайна (classify → group → compare) и дал 30 кейсов,
заточенных под конкретные уязвимости реализации.
---
## Моя оценка
### Сильные стороны
1. **Опус реально читал код.** Он нашёл `_smart_extract` (первые 1500 символов + regex-маркеры),
`normalize_number` (диапазон `А-Я` не включает `Ё`), `new_values` содержит ТОЛЬКО изменённые поля.
Это не общие рекомендации — это точечные удары по слабым местам.
2. **Кейсы 13-14 — золото.** Кириллическая `О` vs ноль `0`, латинская `C` vs кириллическая `С`
это реально ломает группировку. Опус предлагает их как «баг-детекторы»: не исправлять,
а задокументировать текущее поведение и ждать fuzzy-нормализации.
3. **Кейс 15 (Ё)** — я даже не знал про диапазон `А-Я`. Опус нашёл.
4. **Приоритет:** Блок B (group) → Блок C (compare) → Блок A (classify). Правильно —
group ломается детерминированно без LLM, баги воспроизводимы.
5. **Кейс 9 (слепая зона)** — 1500 символов выжимки. Практически важный кейс,
может объяснить почему некоторые файлы не классифицируются.
### Что можно добавить
- **Кейс на batch-progress при падении воркера:** если `classify_worker` упал на середине,
progress застревает на N/69 и никогда не достигнет total. Фронт висит вечно.
- **Кейс на два classify подряд с одним batch_id:** наш lock-файл должен вернуть 409.
Стоит проверить что второй запрос действительно отклоняется.
### Итого
30 кейсов покрывают все три шага пайплайна. ~40% кейсов — group (самый хрупкий),
~30% — compare (LLM-зависимый), ~30% — classify.
Можно брать в реализацию. Я генерирую docx по этим шаблонам.
---
## Исходный ответ Опуса
*Далее — полный текст ответа Опуса без сокращений.*
### Кейс 1: Эталонный договор (baseline)
**Что проверяем:** базовое извлечение всех 6 полей из чистого договора.
**Почему может сломаться:** если падает даже это — проблема не в данных, а в промпте/парсинге.
**Файлы:** договор-XXX001-03700.docx
**Ключевой текст договора:** "Договор № XXX001-03700 на оказание технологических услуг от 15 марта 2025 г. ООО «Облако-Сервис» (Исполнитель)…"
**Ожидаем:** doc_type=contract, own_number="XXX001-03700", parent_number=null, doc_date="2025-03-15", counterparty="ООО «Облако-Сервис»", confidence=ok
### Кейс 2: Номер кириллицей
**Что проверяем:** own_number с кириллическим префиксом и слешем.
**Почему может сломаться:** LLM может «перевести» кириллицу в латиницу или отбросить год после слеша.
**Файлы:** договор-МЭС.docx
**Ключевой текст договора:** "Договор № МЭС-123/2024 от 10.01.2024 г."
**Ожидаем:** own_number="МЭС-123/2024" дословно (важно для парного group-кейса 12).
### Кейс 3: Нестандартный заголовок (не слово «Договор»)
**Что проверяем:** определение doc_type=contract, когда документ называется иначе.
**Почему может сломаться:** LLM привязывается к слову «Договор»; «Соглашение об оказании услуг» может уехать в other.
**Файлы:** договор-нестандарт.docx
**Ключевой текст договора:** "СОГЛАШЕНИЕ об оказании услуг связи № SVC-77 от 01.02.2025"
**Ожидаем:** doc_type=contract (а не other).
### Кейс 4: Допник с явным родителем
**Что проверяем:** разделение own_number и parent_number.
**Почему может сломаться:** LLM путает «свой» номер ДС и номер базового договора местами.
**Файлы:** допник-1-XXX003-01300_2.docx
**Ключевой текст договора:** "Дополнительное соглашение № 1 к Договору № XXX003-01300 от 05.06.2024"
**Ожидаем:** doc_type=supplement, own_number="1", parent_number="XXX003-01300".
### Кейс 5: Спецификация как отдельный файл
**Что проверяем:** doc_type=specification и привязка parent_number.
**Почему может сломаться:** спека без слова «договор» в шапке → other; parent потеряется.
**Файлы:** спецификация-XXX001-03700.docx
**Ключевой текст договора:** "Спецификация № 1 к Договору № XXX001-03700"
**Таблица спеки:** № / Наименование / Цена / Объём / Сумма / Дата.
**Ожидаем:** doc_type=specification, parent_number="XXX001-03700".
### Кейс 6: Договор БЕЗ контрагента в шапке
**Что проверяем:** поведение, когда counterparty не извлекается.
**Почему может сломаться:** LLM «галлюцинирует» контрагента или ставит реквизиты вместо названия.
**Файлы:** договор-без-стороны.docx
**Ключевой текст договора:** "Договор № NC-09 от 03.03.2025 на оказание услуг" (стороны — только в конце документа, см. кейс 9).
**Ожидаем:** counterparty=null/"" , confidence=low (а не выдуманное ООО).
### Кейс 7: Дата прописью и в нестандартном формате
**Что проверяем:** нормализацию doc_date → YYYY-MM-DD.
**Почему может сломаться:** «пятнадцатое марта две тысячи двадцать пятого года» или «15.03.25» (двузначный год).
**Файлы:** договор-дата-прописью.docx
**Ключевой текст договора:** "Договор № DT-15 от «пятнадцатого» марта 2025 года"
**Ожидаем:** doc_date="2025-03-15".
### Кейс 8: Несколько дат в шапке (дата vs срок действия)
**Что проверяем:** выбор ПРАВИЛЬНОЙ даты (дата заключения, а не «действует до»).
**Почему может сломаться:** LLM хватает первую попавшуюся дату.
**Файлы:** договор-две-даты.docx
**Ключевой текст договора:** "Договор № TD-21 от 01.04.2025, действует до 31.12.2026"
**Ожидаем:** doc_date="2025-04-01".
### Кейс 9: Реквизиты за пределами первых 1500 символов
**Что проверяем:** «слепую зону» _smart_extract.
**Почему может сломаться:** номер/контрагент стоят после длинной преамбулы (>1500 симв.) и далеко от regex-маркеров → в выжимку не попадут.
**Файлы:** договор-длинная-преамбула.docx
**Ключевой текст договора:** первые 2 страницы — общие положения без слова «№»; и только потом "Договор № LATE-99 … ООО «Поздний Контрагент»".
**Ожидаем:** документ должен классифицироваться (маркер №/договор рядом с данными). Если падает — это сигнал расширить окно выжимки.
### Кейс 10: «Шумный» документ — несколько номеров на странице
**Что проверяем:** выбор own_number среди нескольких «№».
**Почему может сломаться:** в шапке есть «Исх. № 456», «Лиц. № 789» и сам «Договор № MN-01» → LLM берёт чужой номер.
**Файлы:** договор-много-номеров.docx
**Ключевой текст договора:** "Исх. № 456 от 12.05.2025 … Лицензия № 789 … ДОГОВОР № MN-01 от 12.05.2025"
**Ожидаем:** own_number="MN-01".
### Кейс 11: doc_type=other (мусорный файл)
**Что проверяем:** что не-договор уходит в other, а не натягивается на contract.
**Почему может сломаться:** LLM «обязательно» хочет найти договор.
**Файлы:** акт-сверки.docx
**Ключевой текст договора:** "Акт сверки взаимных расчётов за 1 квартал 2025"
**Ожидаем:** doc_type=other, confidence=low.
### Кейс 12: Разделители — нормализуются (позитив)
**Что проверяем:** «МЭС-123/2024» и «МЭС 123/2024» → одна группа.
**Почему может сломаться:** baseline нормализации; обе дают МЭС1232024.
**Файлы:** договор-МЭС.docx (own="МЭС-123/2024") + допник-МЭС.docx (parent="МЭС 123/2024")
**Ожидаем:** документы в ОДНОЙ группе.
### Кейс 13: Кириллическая «О» против нуля «0» (классическая опечатка)
**Что проверяем:** «O3700» с кириллической О против «03700» с нулём.
**Почему может сломаться:** код НЕ приравнивает кириллицу к цифрам → О3700 ≠ 03700 → допник осиротеет в __unresolved__.
**Файлы:** договор.docx (own="XXX001-03700", цифра ноль) + допник.docx (parent="XXX001-О3700", кириллическая О)
**Ожидаем (как баг-детектор):** сейчас попадут в РАЗНЫЕ группы. Кейс фиксирует поведение и проверяет, появится ли fuzzy-нормализация.
### Кейс 14: Латинская «C» против кириллической «С»
**Что проверяем:** визуально одинаковые префиксы из разных алфавитов.
**Почему может сломаться:** normalize_number сохраняет оба алфавита → CBC-10 (лат) ≠ СВС-10 (кир).
**Файлы:** договор.docx (own="CBC-10", латиница) + допник.docx (parent="СВС-10", кириллица)
**Ожидаем (баг-детектор):** разные группы. Маркер необходимости юникод-конфьюзабл нормализации.
### Кейс 15: Буква «Ё» в номере
**Что проверяем:** диапазон А-Я не включает Ё.
**Почему может сломаться:** normalize_number("ЁЖ-5")="Ж5" — буква Ё выпадает. Если в одном документе «ЁЖ-5», в другом «ЖЕ-5» — рассинхрон.
**Файлы:** договор.docx (own="ЁЖ-5") + допник.docx (parent="ЁЖ-5")
**Ожидаем:** оба теряют Ё одинаково → совпадут как Ж5 (позитив, но по «неправильной» причине — кейс это документирует).
### Кейс 16: parent_number отсутствует у допника
**Что проверяем:** ветку «осиротевших» документов.
**Почему может сломаться:** допник без parent и без own-номера уходит в __unresolved__.
**Файлы:** допник-без-родителя.docx
**Ключевой текст договора:** "Дополнительное соглашение к договору оказания услуг" (без номеров вообще)
**Ожидаем:** документ в группе __unresolved__, не приклеен к случайному договору.
### Кейс 17: Допник ссылается на own_number, а не parent
**Что проверяем:** ветку матчинга «parent==c_norm ИЛИ own==c_norm».
**Почему может сломаться:** если LLM записал номер базового договора в own_number допника (а parent=null), группировка всё равно должна склеить.
**Файлы:** договор.docx (own="GR-50") + допник.docx (own="GR-50", parent=null)
**Ожидаем:** одна группа (срабатывает ветка own==own).
### Кейс 18: Два РАЗНЫХ договора с одинаковым нормализованным номером
**Что проверяем:** коллизию якорей групп.
**Почему может сломаться:** «AB-12» и «A-B12» → оба AB12; допник приклеится не к тому/к обоим.
**Файлы:** договор-A.docx (own="AB-12") + договор-B.docx (own="A-B12") + допник.docx (parent="AB12")
**Ожидаем:** видно недетерминированность/двойную привязку — кейс ловит коллизии нормализации.
### Кейс 19: Семья из 4 документов, разный порядок дат
**Что проверяем:** сортировку внутри группы по doc_date и метку initial/additional.
**Почему может сломаться:** если даты парсятся криво, «initial» может стать не самый ранний документ.
**Файлы:** договор(2025-01-10) + допник-2(2025-05-01) + спека(2025-02-01) + допник-1(2025-03-01)
**Ожидаем:** порядок initial=договор, далее по возрастанию даты; type первого = initial.
### Кейс 20: Допник с лишним суффиксом-копией в имени файла
**Что проверяем:** что нормализуется НОМЕР, а не имя файла.
**Почему может сломаться:** имя «допник-1-XXX003-01300_2.docx» содержит _2 (копия), это не должно влиять на own_number/parent.
**Файлы:** допник-1-XXX003-01300_2.docx
**Ключевой текст договора:** "Дополнительное соглашение № 1 к Договору № XXX003-01300"
**Ожидаем:** own_number="1", parent_number="XXX003-01300"; _2 игнорируется.
### Кейс 21: Цена изменилась на 1 копейку
**Что проверяем:** чувствительность UPDATE к микроизменению.
**Почему может сломаться:** LLM сочтёт разницу «несущественной» и не выдаст UPDATE; или округлит.
**Файлы:** спека-v1.docx + допник-цена.docx
**Таблица спеки (current):** Аренда стойко-места | 50000.00 | 1 | 50000.00 | 2025-01-01
**Текст допника:** "С 01.03.2025 стоимость аренды устанавливается 50 000,01 руб."
**Ожидаем:** UPDATE r1 new_values={price:50000.01, sum:50000.01, date_start:"2025-03-01"}.
### Кейс 22: Объём с 3 на 0 — это UPDATE или DELETE?
**Что проверяем:** трактовку «количество стало нулём».
**Почему может сломаться:** граница UPDATE(qty=0) vs DELETE; разные модели решают по-разному.
**Файлы:** спека-v1.docx + допник-обнуление.docx
**Таблица спеки (current):** IP-адрес IPv4 | 300 | 3 | 900 | 2025-01-01
**Текст допника:** "С 01.04.2025 услуга предоставления IP-адресов исключается (количество — 0)."
**Ожидаем (фиксируем решение):** один из {DELETE r1} ИЛИ {UPDATE r1 qty=0,sum=0}. Кейс закрепляет ожидаемую трактовку и ловит непостоянство.
### Кейс 23: Услуга переименована, суть та же
**Что проверяем:** семантический матч UPDATE по смыслу, а не по символам.
**Почему может сломаться:** LLM не свяжет «Аренда стойко-места» и «Размещение оборудования в стойке» → выдаст ADD+DELETE вместо UPDATE.
**Файлы:** спека-v1.docx + допник-переименование.docx
**Таблица спеки (current):** Аренда стойко-места | 50000 | 1 | 50000 | 2025-01-01
**Текст допника:** "Услугу «Размещение оборудования в стойке» с 01.05.2025 — 52 000 руб."
**Ожидаем:** UPDATE r1 (а не ADD новой + DELETE старой).
### Кейс 24: Полная замена приложения (full_replace)
**Что проверяем:** триггер mode=full_replace по фразе «изложить в следующей редакции».
**Почему может сломаться:** LLM попытается diff'ить построчно (partial) вместо того, чтобы выдать все строки как ADD.
**Файлы:** спека-v1.docx + допник-новая-редакция.docx
**Таблица спеки (current):** r1 Аренда | 50000 | 1 | 50000; r2 IP | 300 | 8 | 2400
**Текст допника:** "Приложение № 1 изложить в следующей редакции:" + новая таблица (Аренда 55000; IP 12 шт; +Резервное копирование 4000).
**Ожидаем:** mode=full_replace, ВСЕ строки новой редакции как ADD, без UPDATE/DELETE.
### Кейс 25: Добавление новой услуги (чистый ADD)
**Что проверяем:** распознавание строки, которой не было.
**Почему может сломаться:** LLM попробует «прицепить» к похожей существующей через UPDATE.
**Файлы:** спека-v1.docx + допник-добавление.docx
**Таблица спеки (current):** Аренда | 50000 | 1 | 50000
**Текст допника:** "С 01.06.2025 добавить услугу «Резервное копирование 1 ТБ» — 4 000 руб./мес., 1 шт."
**Ожидаем:** ADD new_row={name:"Резервное копирование 1 ТБ", price:4000, qty:1, sum:4000, date_start:"2025-06-01"}.
### Кейс 26: Удаление услуги (чистый DELETE)
**Что проверяем:** корректный target_id при удалении.
**Почему может сломаться:** LLM удалит не ту строку (перепутает r1/r2) или выдаст UNRESOLVED.
**Файлы:** спека-v1.docx + допник-удаление.docx
**Таблица спеки (current):** r1 Аренда | 50000 | 1 | 50000; r2 Мониторинг | 2000 | 1 | 2000
**Текст допника:** "С 01.07.2025 услуга «Мониторинг 24/7» исключается из спецификации."
**Ожидаем:** DELETE r2 (именно r2).
### Кейс 27: Изменение только суммы при тех же цене×объёме (ловушка консистентности)
**Что проверяем:** что LLM не «досчитывает» поля, которых нет в допнике.
**Почему может сломаться:** допник меняет только qty, а LLM забывает пересчитать sum (или наоборот, лезет в price).
**Файлы:** спека-v1.docx + допник-объём.docx
**Таблица спеки (current):** IP-адрес | 300 | 8 | 2400 | 2025-01-01
**Текст допника:** "Увеличить количество IP-адресов до 12 (с 01.08.2025)."
**Ожидаем:** UPDATE r1 new_values={qty:12, sum:3600, date_start:"2025-08-01"} — price НЕ в new_values.
### Кейс 28: Допник меняет услугу, которой нет в спеке (UNRESOLVED)
**Что проверяем:** ветку UNRESOLVED.
**Почему может сломаться:** LLM «придумает» ADD вместо честного UNRESOLVED.
**Файлы:** спека-v1.docx + допник-призрак.docx
**Таблица спеки (current):** Аренда | 50000 | 1 | 50000
**Текст допника:** "Стоимость услуги «Услуга миграции данных» снизить на 10%." (такой услуги в спеке нет)
**Ожидаем:** UNRESOLVED reason="услуги нет в текущей спецификации".
### Кейс 29: Числа с пробелами-разделителями и запятой-десятичной
**Что проверяем:** парсинг «55 000,00» → 55000.0.
**Почему может сломаться:** LLM вернёт строку «55 000,00» или 55.0 (обрежет по запятой).
**Файлы:** спека-v1.docx + допник-формат-чисел.docx
**Таблица спеки (current):** Аренда | 50000 | 1 | 50000
**Текст допника:** "Цена аренды с 01.09.2025 — 55 000,00 руб."
**Ожидаем:** UPDATE r1 price=55000.0 (число, не строка).
### Кейс 30: Болтливый LLM-ответ / JSON в markdown (робастность парсера)
**Что проверяем:** устойчивость парсинга на стороне Python (compare и classify).
**Почему может сломаться:** ответ обёрнут в ```json ```, есть текст «Вот результат:», висячая запятая.
**Файлы:** любой простой допник (UPDATE одной цены) — суть в форме ответа, не в данных.
**Текст допника:** "Цена аренды — 51 000 руб. с 01.10.2025."
**Ожидаем:** парсер извлекает JSON из markdown-блока и применяет UPDATE r1.
### Матрица покрытия
| Аспект | Кейсы |
|---|---|
| classify: все поля / baseline | 1, 5 |
| classify: тип документа (contract/spec/other) | 3, 5, 11 |
| classify: own vs parent | 4, 17 |
| classify: дата | 7, 8 |
| classify: контрагент | 1, 6 |
| classify: слепая зона выжимки | 9, 10 |
| group: нормализация разделителей | 12, 15 |
| group: кириллица/латиница/цифры | 13, 14, 15 |
| group: сироты / unresolved | 16 |
| group: ветки матча и коллизии | 17, 18 |
| group: сортировка/порядок | 19, 20 |
| compare: UPDATE | 21, 23, 27, 29 |
| compare: ADD / DELETE | 22, 25, 26 |
| compare: full_replace | 24 |
| compare: UNRESOLVED | 22, 28 |
| robustness: JSON-парсинг | 30 |
**Рекомендация по приоритету:** сначала Блок B (кейсы 13–18) — там код ломается детерминированно и без LLM, баги воспроизводимы на 100%. Потом Блок C (LLM-логика), затем Блок A.
+41
View File
@@ -0,0 +1,41 @@
Ты — Claude Opus. Тебе пишет разработчик.
**ВАЖНО:** Твой ответ — ТОЛЬКО текст в чат. Ты НЕ должен ничего редактировать, создавать файлы, писать код. Просто текстовый ответ с принципами и шаблонами. Файлы создам я сам.
У нас сервис сверки договоров. Pipeline: загрузка → LLM-классификация → группировка → LLM-сравнение.
Бэкенд: Python http.server + PostgreSQL. LLM: gpt-oss-120b через api.aillm.ru.
Реальные файлы лежат в `dogovora/примеры_договоров_для_ИИ/`:
- договор-XXX001-03700.docx — Договор на оказание технологических услуг
- допник-1-XXX003-01300_2.docx — Допсоглашение с таблицей услуг (6-7 колонок: №, Наименование, Цена, Объем, Сумма, Дата)
- спецификация-XXX001-03700.docx — Спецификация (таблица услуг)
Мне не нужны сгенерированные ТОБОЙ docx-файлы — это дорого.
Мне нужны ПРИНЦИПЫ + ТЕКСТОВЫЕ ШАБЛОНЫ для 25-30 тестовых кейсов. Каждый кейс проверяет один конкретный аспект:
- classify (как LLM извлекает тип/номер/дату/контрагента/родителя)
- group (как Python нормализует номера и группирует)
- compare (как LLM находит ADD/DELETE/UPDATE между версиями спеки)
Формат ответа — для каждого кейса:
```
### Кейс N: Название
**Что проверяем:** ...
**Почему может сломаться:** ...
**Файлы:** договор-X.docx + спека-X.docx
**Ключевой текст договора:** "Договор № ... от ..."
**Таблица спеки:** <структура с ценами/объёмами>
```
Файлы создам я сам через python-docx. Ты даёшь принципы и текст — я генерирую.
Примеры того ЧТО может пойти не так и что надо проверить:
- Номер договора кириллицей: "МЭС-123/2024"
- Два допника ссылаются на номер с опечаткой: "03700" vs "О3700"
- Спека без даты
- Договор без контрагента
- Нестандартный заголовок: "Соглашение об услугах" вместо "Договор"
- Compare: цена изменилась на 1 копейку, объём с 3 на 0, услуга переименована
Думай как тестировщик: что МОЖЕТ сломаться и как это поймать.
+97
View File
@@ -0,0 +1,97 @@
# Ответы заказчика — опросник «Сверка договоров»
Дата: 26.06.2026 | Сергей Мищук ↔ Владимир Крупский
---
## 1. Объём
**Вопрос:** Сколько файлов обычно загружаете за один раз?
- A. до 1020
- B. 50100
- C. 100+ (сотни/тысячи)
**Ответ:****C. 100+ (сотни/тысячи)**
---
## 2. Названия НУБЕС в договорах
**Вопрос:** Под какими названиями НУБЕС встречается в документах?
**Ответ:** Есть официальное название. Во всех интересующих нас договорах — Исполнитель.
---
## 3. «Мусорные» документы
**Вопрос:** Какие можно игнорировать при сверке?
**Ответ:** Все, кроме перечисленных (спецификация и дополнительное соглашение, возможно договор).
Т.е. игнорировать: акты сверки, счета/счета-фактуры/УПД, акты оказанных услуг, платёжные поручения. Оставлять: договоры, доп. соглашения, спецификации.
---
## 4. ZIP-архивы
**Вопрос:** Обычно один архив = один контрагент?
**Ответ:** Чаще всего 1 архив = 1 контрагент, но гарантировать не могу.
Размеры ZIP-файлов: не громадные.
---
## 5. Что важнее всего в результате
**Ответ:** Разобранный результат. Окончательная задача — **построчное сравнение данных из CRM с данными из фискальной системы**. Разница может быть:
- в количестве или ценах (ошибки ввода, ошибки процесса)
- особенно в **разных датах начала оказания услуг**
- услуги PAYG (суффикс `-m`), но кодов артикулов, вероятно, нет в счетах
**Сверку с CRM тоже можно делегировать AI.**
---
## Уточняющие вопросы (после опросника)
| Время | Вопрос (Крупский) | Ответ (Мищук) |
|---|---|---|
| 15:34 | Размеры документов не громадные? ZIP-файлов вернее. | Нет. |
| 15:36 | Это за месяц, квартал? | Могут быть разные потребности. Сверить по контрагенту, сверить за год, за месяц. |
| 15:37 | ZIP'ы — ввод из локали или например S3? | Не знаю. Спокойнее из локали, но может быть долго. |
| 15:37 | Т.е. по ссылке. | *(Крупский)* Пусть пока из локали, но сделаю с возможностью расширения. |
| 15:39 | — | Можно из файловой шары или облачного диска. Как работать с файлами — часть проекта, а не ТЗ. В ТЗ ограничение на строгую конфиденциальность → не хочется класть на публичный S3, мало ли кто ошибётся. |
| 15:40 | Модуль ввода сделаю типа API. | — |
| 15:41 | — | **Нас пока интересует не удобство загрузки, а точность анализа.** Скорее всего будем выявлять точечные ошибки и всё равно проверять глазами. Но пока не знаем масштаба расхождений и точности AI. |
| 15:42 | Ну да... трудно прогнозировать. Поэтому хардкодить логику не надо. | — |
| 15:42 | — | Удобство интерфейса пока — только для отладки. Сейчас оснастка выглядит полезной. |
| 15:43 | — | **Надо сначала отработать базовую функцию — разбор, сверка.** Если подход и точность устраивают — можно допилить для юзабельности. |
| 15:43 | — | Сейчас и с подходом всё неясно: что в контекст, что в RAG, что в базу, какая архитектура агентов... можно по-разному. |
| 15:44 | Посмотрю ещё по best practices. | — |
| 15:44 | — | Если интервью всё — пожелаю успехов и пойду к другим задачам? |
| 15:45 | ОК 😊 | — |
| 15:45 | — | Да, я бы хотел **картинок с вариантами архитектуры**. Видел, как AI сама такие рисует. |
| 15:45 | Дам задачу. | — |
| 15:46 | Для себя делал, но некрасиво и коротко. | — |
| 15:49 | — | Надо сначала посмотреть, может без красот обойдёмся. Это всё ради понимания. |
| 16:42 | Если файлов много — может их не надо все списком в таблице выводить? Показывать только статистику — количество по типам, размеры, что невозможно распарсить и т.д. | — |
| 17:11 | — | Зависит от того, что будем делать в интерфейсе. Например, посмотреть чего напарсили — нужен список. Если пока без этого — можно без списка. Но со списком будет удобнее отлаживать. |
| 17:12 | Либо по умолчанию список свёрнут. | — |
| 17:13 | Но это детали, там скроллинг. | — |
---
## Ключевые выводы
1. **Объём: 100+ файлов** → нужен пакетный режим, не поштучная загрузка.
2. **НУБЕС = Исполнитель** во всех целевых договорах.
3. **Оставлять только договоры/ДС/спецификации**, остальное игнорировать.
4. **1 архив ≈ 1 контрагент**, но не гарантировано.
5. **Главное — точность разбора**, а не удобство интерфейса. Сначала базовая функция, потом юзабельность.
6. **Конечная цель — сравнение CRM ↔ фискальная система**, с фокусом на даты начала услуг.
7. **PAYG** (суффикс `-m`) — особый случай.
8. **Сверку с CRM тоже можно делегировать AI.**
9. **Загрузка из локали** (файловая шара / облачный диск), не S3 (конфиденциальность).
10. **Архитектура неясна** — нужно исследовать подходы (контекст, RAG, база, агенты).