6.6 KiB
6.6 KiB
Сессия 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.cfmv2 переиспользует ту же логику textify (elements_json → текст), что и v1parser.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, миграция не нужна
Технические детали реализации
Таблицы (новые)
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