125 lines
6.6 KiB
Markdown
125 lines
6.6 KiB
Markdown
# Сессия 12 — Архитектура Event Sourcing v2
|
||
|
||
Дата: 2026-06-20
|
||
|
||
## Контекст
|
||
|
||
Обсуждение новой архитектуры сравнения договоров: переход от pairwise diff по `row_num`
|
||
к Event Sourcing с LLM-интерпретацией допсоглашений (ДС).
|
||
|
||
Участники: DeepSeek (этот агент), Gemini, Sonnet.
|
||
|
||
## Ключевые решения
|
||
|
||
### 1. Разделение труда
|
||
- **LLM** переводит текст ДС → список операций (ADD/UPDATE/DELETE/UNRESOLVED)
|
||
- **Lucee** исполняет операции как лог, собирает кумулятивный статус
|
||
- LLM не участвует в математике сравнения
|
||
|
||
### 2. Хэш вместо row_num
|
||
- `md5(lower(trim(name)) || coalesce(date_start,''))` — только неизменяемые идентификаторы
|
||
- `qty`, `price`, `sum` НЕ входят в хэш — это изменяемые атрибуты
|
||
- Если `qty` в хэше: ДС меняет количество → новый хэш → ADD вместо UPDATE → дубликат
|
||
- Уникальность: имя + дата достаточно (две строки с одинаковым именем и датой в одном договоре — крайний случай, решается уточнением name)
|
||
|
||
### 3. Старый код
|
||
- `spec_rows` + старый differ остаются параллельно (prod не трогаем)
|
||
- Первичный договор: старый экстрактор строк
|
||
- Допники: новый движок с операциями
|
||
- После обкатки — старый отключаем
|
||
|
||
### 4. ВМ-прокси как async-буфер для LLM
|
||
- Lucee через cfhttp висит синхронно до 120с
|
||
- ВМ (Flask + httpx) забирает задачу, вызывает LLM, отдаёт готовый JSON
|
||
- Новый endpoint `/llm-ops` в том же `convert_server.py`
|
||
- Допники обрабатываются по одному (SSE прогресс)
|
||
|
||
### 5. LLM-промпт (новый, для ДС)
|
||
- Контекст: полная текущая спецификация (hash + все поля) + текст ДС
|
||
- 100 строк ~10KB — для 120B модели не проблема
|
||
- LLM сам определяет full_replace vs изменения построчно
|
||
- Возвращает частичные изменения (только изменённые поля), Lucee делает COALESCE
|
||
|
||
### 6. БД: Event Sourcing
|
||
- `spec_events`: лог операций (contract_id, supplement_id, seq INTEGER per-contract, action, target_hash, new_values JSONB, comment)
|
||
- `spec_current`: текущий статус (таблица, обновляется при apply)
|
||
- full_replace → явные DELETE на каждую строку (аудит)
|
||
- Весь ДС — одна транзакция (атомарность)
|
||
- Откат целиком по supplement_id
|
||
|
||
### 7. UNRESOLVED
|
||
- Тип операции уже определён LLM (ADD/UPDATE/DELETE)
|
||
- UNRESOLVED — LLM не смог найти хэш для привязки
|
||
- Оператор в `resolve.cfm`: выпадайка с существующими услугами → привязать хэш
|
||
- Применяется как UPDATE к выбранной услуге
|
||
|
||
### 8. UI
|
||
- `resolve.cfm` — отдельная страница для ручного разрешения
|
||
- Список UNRESOLVED строк, дропдаун с услугами, кнопка «Подтвердить»
|
||
- Текущий `view.cfm` — предпросмотр документа (первые 15 строк)
|
||
|
||
### 9. Триггер пайплайна
|
||
- `process.cfm?contract_id=X&v=2` — новая версия
|
||
- Старая кнопка = v1, новая кнопка = v2 (рядом)
|
||
- Никаких флагов в БД, никакого auto-detect
|
||
- Когда обкатаем → переключаем дефолтную кнопку
|
||
|
||
### 10. parsed_text для ДС
|
||
- `process.cfm` v2 переиспользует ту же логику textify (elements_json → текст), что и v1
|
||
- `parser.cfm` НЕ трогать
|
||
- Текст формируется в process.cfm и передаётся на ВМ
|
||
|
||
### 11. Первичное заполнение spec_current
|
||
- `process.cfm?v=2` обрабатывает все supplements с нуля, по порядку
|
||
- Initial: `current_spec = []` (пустой) → LLM возвращает всё как ADD → spec_current заполнен
|
||
- Каждый следующий ДС: `current_spec` из уже заполненного spec_current
|
||
- `spec_rows` не трогаем — старый пайплайн независим
|
||
- Для существующих контрактов: v2 пересчитывает с нуля через LLM, миграция не нужна
|
||
|
||
## Технические детали реализации
|
||
|
||
### Таблицы (новые)
|
||
```sql
|
||
spec_events (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
contract_id UUID REFERENCES contracts(id),
|
||
supplement_id UUID REFERENCES supplements(id),
|
||
seq INTEGER NOT NULL, -- per-contract счётчик
|
||
action TEXT NOT NULL, -- ADD, UPDATE, DELETE, UNRESOLVED
|
||
target_hash TEXT, -- хэш услуги
|
||
new_values JSONB, -- для ADD/UPDATE
|
||
comment TEXT,
|
||
resolved_by TEXT, -- кто разрешил UNRESOLVED
|
||
created_at TIMESTAMPTZ DEFAULT now(),
|
||
UNIQUE(contract_id, seq)
|
||
)
|
||
|
||
spec_current (
|
||
contract_id UUID REFERENCES contracts(id),
|
||
name_hash TEXT NOT NULL, -- md5(lower(trim(name)) || coalesce(date_start,''))
|
||
name TEXT,
|
||
price NUMERIC,
|
||
qty NUMERIC,
|
||
sum NUMERIC,
|
||
date_start TEXT,
|
||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||
PRIMARY KEY (contract_id, position_hash)
|
||
)
|
||
```
|
||
|
||
### API ВМ `/llm-ops`
|
||
```
|
||
POST /llm-ops
|
||
body: {contract_id, supplement_id, current_spec: [{hash, name, price, qty, sum, date_start}], doc_text: "..."}
|
||
returns: {operations: [{action, target_hash, new_values, comment}], mode: "partial"|"full_replace"}
|
||
```
|
||
|
||
## Связанные файлы
|
||
- Запрос Sonnet: `contractor/Files/sonnet-v2-request.md`
|
||
- Старый код парсинга: `contractor/parser.cfm`
|
||
- Старый код сравнения: `contractor/differ.cfm`
|
||
- Старый код экстракции: `contractor/extractor.cfm`
|
||
- Текущий process: `contractor/process.cfm`
|
||
- ВМ-прокси: `contractor/deploy/convert_server.py`
|
||
- ВМ-nginx: `contractor/deploy/nginx-contracts.conf`
|