a8f7e68795
Оператор-настройщик после правки промпта _router нажимает «Прогнать выбранное»
на странице «Регрессия» и видит, что сломалось. Не CLI, не в обход
интерфейса — встроено в верхнюю навигацию рядом с Настройками.
Backend:
- Таблицы eval_runs / eval_run_cases (с is_pass) / eval_router_predictions
(кэш text_hash + router_config_id → predicted_intent). Миграции
k7e9d5c67h34 и l8f0e6d78i45.
- services/eval_run_service.py: start_router_run(text_hashes) запускает
фоновую корутину через asyncio.create_task, фиксирует активную версию
_router. Кэш привязан к версии: повторный прогон на той же версии —
мгновенный, на новой — пересчитывается. compute_diff_vs_previous
сравнивает с предыдущим прогоном на той же версии (новые fail / pass).
- API: POST /eval/runs (фон, body text_hashes), GET /eval/runs,
GET /eval/runs/{id}, GET /eval/router-cases-with-status (все 1573 кейса
+ кэш на активной версии).
Frontend (static/regression.html — новая страница, ссылка добавлена в
шапки index/sandbox/settings/docs):
- Сворачиваемый блок «Выбор кейсов»: фильтр по intent, ввод диапазона
(1-50, 200-300), кнопки «Все видимые», «Снять все», «Только без кэша»,
«Только FAIL в кэше», «Снять кэшированные». Чекбокс в шапке.
- Таблица 1573 кейсов отсортирована по count desc: #, чекбокс, запрос,
intent, частота, кэш (PASS / FAIL → predicted / —). Цветной фон строки
по статусу кэша.
- Счётчик «выбрано N (новых: X, в кэше: Y)»; кнопка
«Прогнать выбранное (X новых + Y из кэша)» — сразу видно реальный
объём LLM-работы.
- Polling /eval/runs/{id} раз в 2 секунды, прогресс-бар, drill-down:
все кейсы прогона + фильтр pass/fail + поиск + diff vs предыдущий
(новые fail / новые pass).
docs/SPRINTS.md: Спринт 8 разбит на 8a (✅ закрыт), 8b (регрессия ответов
веток, ждёт базу кейсов от пользователя), 8c (handoff/resumable/loop/
guard/rag — позже).
docs/BACKLOG.md: новый файл для идей на потом. Записаны: просмотр
архивного графа без активации (из 7.7), варианты C (LLM-judge) и D
(эталон + embeddings) для регрессии веток в 8b.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
84 lines
3.8 KiB
Python
84 lines
3.8 KiB
Python
"""add eval_runs / eval_run_cases / eval_router_predictions (Спринт 8a)
|
|
|
|
Revision ID: k7e9d5c67h34
|
|
Revises: j6d8c4b56g23
|
|
Create Date: 2026-05-02 14:30:00.000000
|
|
|
|
Регрессия роутера в UI:
|
|
- eval_runs — прогон, привязка к версии роутера, статус, статистика.
|
|
- eval_run_cases — только расхождения (predicted != expected).
|
|
- eval_router_predictions — кэш LLM-ответов по (text_hash, router_config_id).
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
|
|
revision: str = 'k7e9d5c67h34'
|
|
down_revision: Union[str, None] = 'j6d8c4b56g23'
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
'eval_runs',
|
|
sa.Column('id', sa.Integer(), nullable=False),
|
|
sa.Column('suite', sa.String(length=50), nullable=False),
|
|
sa.Column('router_config_id', sa.Integer(), nullable=True),
|
|
sa.Column('min_count', sa.Integer(), nullable=False),
|
|
sa.Column('status', sa.String(length=20), nullable=False),
|
|
sa.Column('total', sa.Integer(), nullable=False),
|
|
sa.Column('passed', sa.Integer(), nullable=False),
|
|
sa.Column('failed', sa.Integer(), nullable=False),
|
|
sa.Column('cache_hits', sa.Integer(), nullable=False),
|
|
sa.Column('error_text', sa.Text(), nullable=True),
|
|
sa.Column('started_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('finished_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.ForeignKeyConstraint(['router_config_id'], ['agent_configs.id'], ondelete='SET NULL'),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
)
|
|
op.create_index('ix_eval_runs_router_config_id', 'eval_runs', ['router_config_id'])
|
|
|
|
op.create_table(
|
|
'eval_run_cases',
|
|
sa.Column('id', sa.Integer(), nullable=False),
|
|
sa.Column('run_id', sa.Integer(), nullable=False),
|
|
sa.Column('text', sa.Text(), nullable=False),
|
|
sa.Column('expected_intent', sa.String(length=50), nullable=False),
|
|
sa.Column('predicted_intent', sa.String(length=50), nullable=False),
|
|
sa.Column('count_weight', sa.Integer(), nullable=False),
|
|
sa.ForeignKeyConstraint(['run_id'], ['eval_runs.id'], ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
)
|
|
op.create_index('ix_eval_run_cases_run_id', 'eval_run_cases', ['run_id'])
|
|
|
|
op.create_table(
|
|
'eval_router_predictions',
|
|
sa.Column('id', sa.Integer(), nullable=False),
|
|
sa.Column('text_hash', sa.String(length=64), nullable=False),
|
|
sa.Column('router_config_id', sa.Integer(), nullable=True),
|
|
sa.Column('predicted_intent', sa.String(length=50), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.ForeignKeyConstraint(['router_config_id'], ['agent_configs.id'], ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
sa.UniqueConstraint('text_hash', 'router_config_id', name='uq_eval_router_pred_hash_cfg'),
|
|
)
|
|
op.create_index('ix_eval_router_predictions_text_hash', 'eval_router_predictions', ['text_hash'])
|
|
op.create_index(
|
|
'ix_eval_router_predictions_router_config_id',
|
|
'eval_router_predictions',
|
|
['router_config_id'],
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_index('ix_eval_router_predictions_router_config_id', table_name='eval_router_predictions')
|
|
op.drop_index('ix_eval_router_predictions_text_hash', table_name='eval_router_predictions')
|
|
op.drop_table('eval_router_predictions')
|
|
op.drop_index('ix_eval_run_cases_run_id', table_name='eval_run_cases')
|
|
op.drop_table('eval_run_cases')
|
|
op.drop_index('ix_eval_runs_router_config_id', table_name='eval_runs')
|
|
op.drop_table('eval_runs')
|