v1.0.177: History — объединение history+History, 5 подпапок

This commit is contained in:
“Naeel”
2026-06-25 07:53:56 +04:00
parent 14cca9f8cf
commit c40d8eb5a8
54 changed files with 0 additions and 0 deletions
@@ -0,0 +1,35 @@
# Исправление уязвимостей Event Sourcing — по шагам
**Дата:** 2026-06-22
**Источники:** Gemini Web + Sonnet анализ
**Итог:** все 11 шагов + доп. фиксы выполнены, v1.0.107, сравнение работает.
## Выполненные шаги
| Шаг | Версия | Что | Файл |
|-----|--------|-----|------|
| 1 | v1.0.92 | SQL-инъекция — валидация UUID | convert_server.py |
| 2 | — | Race condition — FOR UPDATE (убран v1.0.100) | apply_events.cfm |
| 3 | v1.0.94 | full_replace guard | apply_events.cfm |
| 4 | v1.0.104 | Идемпотентность + reset_contract.cfm | apply_events.cfm |
| 5 | v1.0.97 | LLM ключ из env | convert_server.py |
| 6 | v1.0.98 | elements_json double-decode | convert_server.py |
| 7 | v1.0.99 | isNumeric валидация | apply_events.cfm |
| 8 | v1.0.101 | Batch SELECT UPDATE | apply_events.cfm |
| 9 | v1.0.102 | UNRESOLVED вместо cfthrow | apply_events.cfm |
| 10 | v1.0.103 | Массовый INSERT full_replace | apply_events.cfm |
| 11 | — | UUID тип — отменён (cf_sql_char ломает PG) | apply_events.cfm |
## Доп. фиксы
| Версия | Что |
|--------|-----|
| v1.0.104 | reset_contract.cfm |
| v1.0.105 | Проверка ответа reset |
| v1.0.106 | Запятая в ADD VALUES |
| v1.0.107 | cf_sql_varchar (UUID cast) |
## Статус
- ✅ v1.0.107 — работает. Summary +N ~N -N ?N. Накопление ОК.
- ⚠️ UPDATE/DELETE без имён в UI (отдельно)
- ⚠️ FOR UPDATE убран (нужен SEQUENCE)
+57
View File
@@ -0,0 +1,57 @@
# План: Мульти-пользовательские промпты
**v1.0.86+**
## Схема БД
```sql
CREATE TABLE IF NOT EXISTS prompts (
id TEXT PRIMARY KEY, -- {user_id}_{timestamp}, например "0000_20260621143000"
user_id TEXT NOT NULL, -- "0000" для тестов, потом Keycloak ID
name TEXT NOT NULL, -- имя промпта
content TEXT NOT NULL, -- тело промпта
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
```
## API: prompt.cfm
| action | Метод | Что |
|--------|-------|-----|
| `list&user_id=0000` | GET | Список промптов юзера |
| `save` | POST `{id?, user_id, name, content}` | Создать (без id) или обновить (с id) |
| `delete&id=...` | GET/POST | Удалить промпт |
| `duplicate&id=...` | GET/POST | Копия с новым ID = user_id + now |
Ответ всегда JSON `{ok: true/false, ...}`.
## UI (index.cfm)
Вместо текущего `<textarea>` + кнопок «Сохранить»/«По умолчанию»:
### Компоненты:
1. `<select>` — выпадающий список промптов (имя + время создания)
2. Кнопка «Новый» — очищает поля, ждёт ввода имени + текста
3. Кнопка «Дублировать» — копирует выбранный с именем «Копия ...»
4. Кнопка «Удалить» — с подтверждением
5. `<input>` — поле имени промпта
6. `<textarea>` — тело промпта (как сейчас)
7. Кнопка «Сохранить» — сохраняет текущий
### Формат времени:
"21.06.2026 14:30" (DD.MM.YYYY HH:MM)
## Порядок реализации
1. **Фаза 1** — БД: таблица `prompts`
2. **Фаза 2** — prompt.cfm: API list/save/delete/duplicate
3. **Фаза 3** — index.cfm: UI с селектом, новым/дубль/удалить/сохранить
4. **Фаза 4** — миграция: текущий промпт из localStorage → первый промпт в БД
## Примечания
- Пока `user_id = "0000"` (тестовый)
- Keycloak: `user_id` = sub из JWT
- Таблица одна на всех юзеров, ID самодостаточен
- localStorage пока оставить как fallback
+28
View File
@@ -0,0 +1,28 @@
# MVP Auto-classification — план реализации
## Итоговый план (Опус + мои правки)
### Фаза 1: БД + данные
1. ALTER documents — 7 колонок (doc_type, own_number, parent_number, doc_date TEXT, counterparty, classify_status, batch_id)
2. db/documents.py — set_classification(), list_pending(batch), list_by_batch(batch)
3. db/prompts.py — seed classify prompt
### Фаза 2: Классификация
4. llm_prompt.py — build_classify_prompt (строгий JSON)
5. services/classify.py — умная выжимка + LLM + ThreadPoolExecutor(4)
### Фаза 3: Группировка
6. services/grouping.py — normalize_number + group_documents + apply_groups
### Фаза 4: Эндпоинты
7. convert_server.py — POST /classify-batch, GET /api/groups, POST /apply-groups
### Фаза 5: UI
8. app.js — batch_id, classify button, polling progress, group cards, per-group compare
9. index.cfm — version bump only (HTML не меняем)
## Мои корректировки к плану Опуса
- batch_id генерируется на клиенте (crypto.randomUUID()), передаётся в upload
- ZIP — фаза 2, сначала multi-upload (уже работает)
- doc_date TEXT (не DATE) — LLM нормализует в ISO, при провале null
- parent_number отдельно от own_number
+168
View File
@@ -0,0 +1,168 @@
# Prompt Management Strategy
Дата: 2026-06-23 | Версия: v1.0 | Контекст: v1.0.109, обсуждение архитектуры промптов
---
## А. Несколько промптов — зачем и как
Смешиваются **две разные потребности** — их нельзя путать:
### 1. Версионирование одного промпта (ОБЯЗАТЕЛЬНО)
Это не «несколько промптов», а история эволюции одного.
**Почему критично:**
- `prompt_version` уже кладётся в `spec_events` (provenance, Opus раунд 2).
- Значит промпт **обязан быть иммутабельным** — правка «на месте» сломает трассировку: старые события будут ссылаться на текст, которого больше нет.
- **Правка = новая версия**, а не перезапись. Откат, аудит «какая версия породила какой результат», сравнение версий — бесплатно.
### 2. Несколько РАЗНЫХ промптов одновременно
| Сценарий | Вердикт |
|----------|---------|
| **По роли в пайплайне** — разные LLM-вызовы (извлечение услуг vs сравнение vs резолв имён) | ✅ У каждого свой промпт. Настоящая причина «несколько» — по **задаче**. |
| **Варианты для эксперимента** — форкнул активный, сделал строже, прогнал на тестах, сравнил | ✅ Loop «дублировать → изменить → протестировать → активировать» — правильно. |
| **Каталог из 20 промптов «на всякий случай»** | ❌ Ловушка. Для узкого домена (colocation/ЦОД) нужен **ОДИН хороший** промпт на задачу. |
### Рекомендуемая модель: prompt-as-version (не prompt-as-document)
Одна таблица, один активный на `role`, вся история — строки:
| Поле | Тип | Смысл |
|------|-----|-------|
| `id` | UUID PK | Версия (иммутабельная) |
| `role` | VARCHAR | Шаг пайплайна: `extract`, `diff` |
| `name` | VARCHAR | Человеческое имя («default-v3», «strict-prices») |
| `body` | TEXT | Тело промпта с плейсхолдерами |
| `parent_id` | UUID FK→prompts.id | От какой версии форкнут |
| `is_active` | BOOLEAN | UNIQUE(role, is_active) с частичным индексом WHERE is_active |
| `created_at` | TIMESTAMP | |
| `created_by` | VARCHAR | Мульти-юзер |
| `notes` | TEXT | Что изменили и зачем |
**Операции:**
- **Править** = `INSERT` новой строки с `parent_id` на предыдущую. Не `UPDATE`.
- **Активировать** = переключить `is_active` (старая теряет флаг).
- **Дублировать** = копия под новым `name` (форк).
- **Откат** = активировать старую версию.
**Никогда не UPDATE текста на месте.**
---
## Б. Как составлять промпт
### Стартовый шаблон — ОБЯЗАТЕЛЕН
Пустое поле ввода — катастрофа для качества. Всегда форкать от выверенного дефолта, не с нуля.
### Структура (8 секций, порядок важен)
Для доменной задачи **без structural output** (gpt-oss-120b):
```
┌─────────────────────────────────────────────┐
│ 1. РОЛЬ / КОНТЕКСТ │
│ «ты эксперт по договорам colocation │
│ и облачных услуг ЦОД» │
├─────────────────────────────────────────────┤
│ 2. ЗАДАЧА │
│ «сравни текущую спецификацию с документом,│
│ верни операции изменений» │
├─────────────────────────────────────────────┤
│ 3. ДОМЕННЫЙ ГЛОССАРИЙ │
│ стойко-место, кВт мощности, colocation, │
│ единицы измерения, типы услуг. │
│ Без этого модель путает домен. │
├─────────────────────────────────────────────┤
│ 4. ФОРМАТ ВХОДА │
│ Как подаётся spec_current (с ID r1..rN), │
│ как подаётся текст документа. │
├─────────────────────────────────────────────┤
│ 5. КОНТРАКТ ВЫВОДА │
│ Строгая JSON-схема ops: поля, типы, │
│ обязательность. КРИТИЧНО — structural │
│ output нет, схема держится промптом. │
├─────────────────────────────────────────────┤
│ 6. ПРАВИЛА / ОГРАНИЧЕНИЯ │
│ «ссылайся только на id из spec_current», │
│ «верни source_quote», «верни confidence», │
│ «name существующих строк не переписывай» │
├─────────────────────────────────────────────┤
│ 7. FEW-SHOT ПРИМЕРЫ (2-3) │
│ ⭐ САМЫЙ МОЩНЫЙ РЫЧАГ для бесплатной │
│ модели без fine-tune. Примеры учат и │
│ формату, и поведению лучше инструкций. │
│ Вход→Выход на ВАШЕМ домене (ЦОД). │
├─────────────────────────────────────────────┤
│ 8. EDGE-CASES │
│ Как помечать неоднозначность, что делать │
│ при low-confidence. │
└─────────────────────────────────────────────┘
```
### Принцип разделения: статика vs рантайм
```
[версионируемая часть — в БД] [рантайм-инъекция — в коде]
роль + глоссарий + правила + {spec_current} + {document_text}
+ few-shot + контракт вывода
```
- Статическая инструкция (секции 1-8) → **версионируется** в `prompts.body`.
- Динамические данные (`{spec_current}`, `{document_text}`, `{date}`) → **подставляются в рантайме** через плейсхолдеры.
- В БД хранится тело промпта **с плейсхолдерами**, не с конкретными данными.
---
## В. Prompt CI — как с этим работать
```
┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐
│ Дублировать │ → │ Изменить │ → │ Прогнать на │ → │ Активировать │
│ (форк) │ │ (новая v) │ │ golden-наборе│ │ (is_active) │
└──────────┘ └──────────┘ └───────────┘ └──────────┘
```
1. Форкнуть активный промпт → новый `id`, `is_active=false`
2. Изменить тело
3. Прогнать на golden-наборе (3-5 реальных ДС с известным результатом)
4. Сравнить метрики (precision/recall по ops)
5. Если лучше — активировать. Если хуже — оставить как эксперимент или удалить.
---
## Г. Текущее состояние (v1.0.109)
Промпты жёстко зашиты в `llm_prompt.py`:
- `_build_initial()` — для первого документа
- `_build_diff()` — для сравнения (основной)
**Что уже хорошо:**
- JSON-контракт вывода описан
- `target_id` (r1..rN) вместо `target_hash`
- Правила: mode full_replace, UPDATE только изменённые поля, UNRESOLVED для неоднозначного
**Чего не хватает (по приоритету):**
1. Few-shot примеры на домене ЦОД — самый большой рычаг качества
2. Доменный глоссарий — чтобы модель понимала единицы измерения и термины
3. `source_quote` в контракте вывода — для provenance
4. `confidence` в контракте вывода — самооценка модели
5. Edge-cases инструкция — как вести себя при неоднозначности
---
## Д. План миграции (если делать)
| Шаг | Что | Сложность |
|-----|-----|-----------|
| 1 | Таблица `prompts` в PostgreSQL (Lucee datasource `baza`) | Низкая |
| 2 | CRUD в `api.cfm` для prompts (list, get, create, set_active) | Средняя |
| 3 | `llm_prompt.py` → читает активный промпт из Lucee API вместо хардкода | Средняя |
| 4 | `prompt_version` в `spec_events` — ссылка на `prompts.id` | Низкая |
| 5 | UI для prompts в `index.cfm` (список версий, редактор, активация) | Высокая |
| 6 | Golden-набор: 3-5 ДС + эталонные ops | Ручная работа |
| 7 | Prompt CI: скрипт прогона на golden-наборе + сравнение метрик | Средняя |
**Первый практический шаг** (максимум пользы, минимум кода): добавить few-shot примеры в текущие промпты прямо в `llm_prompt.py`, не дожидаясь таблицы `prompts`.
+44
View File
@@ -0,0 +1,44 @@
# Provenance Columns — Opus Round 2
Дата: 2026-06-23 | Версия: v1.0.114 | Связано: spec_events, apply_events.cfm, convert_server.py
---
## Что сделано
Добавлены две колонки в `spec_events`, рекомендованные Opus (раунд 2, ответ 6):
| Колонка | Тип | Откуда берётся | Зачем |
|---------|-----|----------------|-------|
| `source_document_id` | UUID FK→documents | `convert_server.py` — из JOIN supplements+documents | Прямая ссылка на исходный документ (provenance). Без неё — только через supplement_id, что хрупко. |
| `raw_llm_response` | JSONB | `convert_server.py` — полный `llm_result` (mode + ops) | Аудит: что модель ВООБЩЕ ответила vs что мы применили. Воспроизводимость. |
Вместе с `prompt_version` (v1.0.113) — полный комплект provenance для каждой операции.
## Почему
Opus: «Добавилась услуга X за 15000» без источника — бесполезно. «Добавилась услуга X ← допник №3, п.2.4, вот абзац» → проверка за 2 секунды.
Три колонки закрывают:
- **Каким промптом** (`prompt_version`)
- **Из какого документа** (`source_document_id`)
- **Что модель ответила** (`raw_llm_response`)
## Что изменилось
### convert_server.py
- SQL запрос supplements: добавлен `d.id as document_id`
- apply_events POST: добавлены `document_id` и `raw_llm_response`
### apply_events.cfm
- 3× ALTER TABLE ADD COLUMN IF NOT EXISTS (prompt_version, source_document_id, raw_llm_response)
- Все 7 INSERT INTO spec_events: +2 колонки с `<cfif len(...)>`
## Что НЕ изменилось
- Логика сравнения
- spec_current
- UI
- Старые строки — NULL в новых колонках, ни на что не влияет
## Риски
- Нулевые. Колонки опциональные (NULL разрешён), чисто аддитивные.