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 EvalBranchRun(Base): """Прогон регрессии конкретной ветки (Спринт 8b). Параллельная сущность к `EvalRun`: тот валидирует роутер (один intent-код в ответе), этот — содержимое ответа конкретной ветки (RAG-секции + keywords). Активная версия промпта ветки фиксируется в `branch_config_id`, кэш ответов привязан к ней — повторный прогон на той же версии мгновенный. """ __tablename__ = "eval_branch_runs" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) suite: Mapped[str] = mapped_column(String(80), nullable=False) intent_code: Mapped[str] = mapped_column(String(50), nullable=False, index=True) branch_config_id: Mapped[int | None] = mapped_column( ForeignKey("agent_configs.id", ondelete="SET NULL"), nullable=True, index=True ) 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 EvalBranchRunCase(Base): """Один кейс прогона ветки. Хранятся все: pass и fail (для фильтра в UI).""" __tablename__ = "eval_branch_run_cases" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) run_id: Mapped[int] = mapped_column( ForeignKey("eval_branch_runs.id", ondelete="CASCADE"), nullable=False, index=True ) text: Mapped[str] = mapped_column(Text, nullable=False) coverage: Mapped[str] = mapped_column(String(20), nullable=False, default="covered") expected_doc_section: Mapped[str | None] = mapped_column(String(300), nullable=True) expected_keywords_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]") expected_must_not_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]") keywords_min: Mapped[int | None] = mapped_column(Integer, nullable=True) predicted_answer: Mapped[str] = mapped_column(Text, nullable=False, default="") predicted_sections_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]") is_pass: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) fail_reasons_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]") count_weight: Mapped[int] = mapped_column(Integer, nullable=False, default=1) class EvalBranchPrediction(Base): """Кэш single-turn ответа ветки: (text_hash, branch_config_id) → {answer, sections}. При активации новой версии ветки кэш для неё пуст; повторный прогон на той же версии берёт всё из кэша, не дёргая LLM. """ __tablename__ = "eval_branch_predictions" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) text_hash: Mapped[str] = mapped_column(String(64), nullable=False, index=True) branch_config_id: Mapped[int | None] = mapped_column( ForeignKey("agent_configs.id", ondelete="CASCADE"), nullable=True, index=True ) answer_text: Mapped[str] = mapped_column(Text, nullable=False, default="") retrieved_sections_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, nullable=False)