session-12: event sourcing architecture v2
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
# Сессия 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`
|
||||
Reference in New Issue
Block a user