"""add eval_branch_runs / cases / predictions (Спринт 8b — регрессия ответов веток) Revision ID: m9g1f7e89j56 Revises: l8f0e6d78i45 Create Date: 2026-05-02 21:30:00.000000 Параллельная сущность к eval_runs (роутер): тут проверяем содержимое ответа конкретной ветки. Кэш — по (text_hash, branch_config_id), хранит answer_text и retrieved_sections для drill-down в UI. """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa revision: str = 'm9g1f7e89j56' down_revision: Union[str, None] = 'l8f0e6d78i45' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.create_table( 'eval_branch_runs', sa.Column('id', sa.Integer(), nullable=False), sa.Column('suite', sa.String(length=80), nullable=False), sa.Column('intent_code', sa.String(length=50), nullable=False), sa.Column('branch_config_id', sa.Integer(), nullable=True), 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(['branch_config_id'], ['agent_configs.id'], ondelete='SET NULL'), sa.PrimaryKeyConstraint('id'), ) op.create_index('ix_eval_branch_runs_intent_code', 'eval_branch_runs', ['intent_code']) op.create_index('ix_eval_branch_runs_branch_config_id', 'eval_branch_runs', ['branch_config_id']) op.create_table( 'eval_branch_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('coverage', sa.String(length=20), nullable=False), sa.Column('expected_doc_section', sa.String(length=300), nullable=True), sa.Column('expected_keywords_json', sa.Text(), nullable=False), sa.Column('expected_must_not_json', sa.Text(), nullable=False), sa.Column('keywords_min', sa.Integer(), nullable=True), sa.Column('predicted_answer', sa.Text(), nullable=False), sa.Column('predicted_sections_json', sa.Text(), nullable=False), sa.Column('is_pass', sa.Boolean(), nullable=False), sa.Column('fail_reasons_json', sa.Text(), nullable=False), sa.Column('count_weight', sa.Integer(), nullable=False), sa.ForeignKeyConstraint(['run_id'], ['eval_branch_runs.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id'), ) op.create_index('ix_eval_branch_run_cases_run_id', 'eval_branch_run_cases', ['run_id']) op.create_table( 'eval_branch_predictions', sa.Column('id', sa.Integer(), nullable=False), sa.Column('text_hash', sa.String(length=64), nullable=False), sa.Column('branch_config_id', sa.Integer(), nullable=True), sa.Column('answer_text', sa.Text(), nullable=False), sa.Column('retrieved_sections_json', sa.Text(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), sa.ForeignKeyConstraint(['branch_config_id'], ['agent_configs.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('text_hash', 'branch_config_id', name='uq_eval_branch_pred_hash_cfg'), ) op.create_index('ix_eval_branch_predictions_text_hash', 'eval_branch_predictions', ['text_hash']) op.create_index( 'ix_eval_branch_predictions_branch_config_id', 'eval_branch_predictions', ['branch_config_id'], ) def downgrade() -> None: op.drop_index('ix_eval_branch_predictions_branch_config_id', table_name='eval_branch_predictions') op.drop_index('ix_eval_branch_predictions_text_hash', table_name='eval_branch_predictions') op.drop_table('eval_branch_predictions') op.drop_index('ix_eval_branch_run_cases_run_id', table_name='eval_branch_run_cases') op.drop_table('eval_branch_run_cases') op.drop_index('ix_eval_branch_runs_branch_config_id', table_name='eval_branch_runs') op.drop_index('ix_eval_branch_runs_intent_code', table_name='eval_branch_runs') op.drop_table('eval_branch_runs')