Files
RAG_helper/migrations/versions/k7e9d5c67h34_add_eval_runs.py
AR 15 M4 a8f7e68795 feat(sprint8a): регрессия роутера в UI с выбором кейсов и кэшем
Оператор-настройщик после правки промпта _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>
2026-05-02 20:39:22 +05:00

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')