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>
This commit is contained in:
@@ -2,6 +2,7 @@ from db.models.agent_config import AgentConfig
|
||||
from db.models.document import Document
|
||||
from db.models.intent import Intent
|
||||
from db.models.intent_document import IntentDocument
|
||||
from db.models.eval_run import EvalRouterPrediction, EvalRun, EvalRunCase
|
||||
from db.models.intent_step import IntentStep
|
||||
from db.models.intent_step_graph import IntentStepGraph
|
||||
from db.models.message import Message
|
||||
@@ -11,4 +12,5 @@ from db.models.thread_state import ThreadState
|
||||
__all__ = [
|
||||
"Thread", "Message", "Document", "AgentConfig", "Intent",
|
||||
"IntentDocument", "IntentStep", "IntentStepGraph", "ThreadState",
|
||||
"EvalRun", "EvalRunCase", "EvalRouterPrediction",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from db.base import Base
|
||||
|
||||
|
||||
def _utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class EvalRun(Base):
|
||||
"""Прогон регрессионного eval-набора (Спринт 8a).
|
||||
|
||||
Сейчас единственный поддерживаемый suite — `router`: одношаговая классификация
|
||||
реплик пациента. Прогон фиксирует активную версию роутера на момент старта
|
||||
(`router_config_id`), чтобы в UI можно было сравнивать прогоны между версиями.
|
||||
|
||||
Кейсы-расхождения хранятся в `eval_run_cases`. Полный список pass+fail не
|
||||
хранится — только статистика; кэш ответов LLM лежит в `eval_router_predictions`.
|
||||
"""
|
||||
__tablename__ = "eval_runs"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
suite: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
router_config_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("agent_configs.id", ondelete="SET NULL"), nullable=True, index=True
|
||||
)
|
||||
min_count: Mapped[int] = mapped_column(Integer, nullable=False, default=2)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="running")
|
||||
total: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
passed: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
failed: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
cache_hits: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
error_text: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, nullable=False)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
|
||||
class EvalRunCase(Base):
|
||||
"""Один кейс прогона (pass или fail).
|
||||
|
||||
Хранятся все кейсы — нужно для UI «таблица кейсов с фильтром pass/fail» и
|
||||
для diff vs предыдущего прогона.
|
||||
"""
|
||||
__tablename__ = "eval_run_cases"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
run_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("eval_runs.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
text: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
expected_intent: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
predicted_intent: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
count_weight: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
is_pass: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
|
||||
|
||||
class EvalRouterPrediction(Base):
|
||||
"""Кэш LLM-предсказаний роутера: ключ (text_hash, router_config_id) → predicted_intent.
|
||||
|
||||
Сильно ускоряет повторные прогоны на той же версии роутера — LLM не дёргаем,
|
||||
берём результат из кэша. При активации новой версии `_router` кэш для неё
|
||||
пуст и первый прогон долгий.
|
||||
"""
|
||||
__tablename__ = "eval_router_predictions"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
text_hash: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||||
router_config_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("agent_configs.id", ondelete="CASCADE"), nullable=True, index=True
|
||||
)
|
||||
predicted_intent: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, nullable=False)
|
||||
Reference in New Issue
Block a user