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)