"""Add conversational branching tables and columns. Revision ID: 067 """ from alembic import op import sqlalchemy as sa from sqlalchemy.dialects.postgresql import UUID, JSONB revision = "067" down_revision = "066" branch_labels = None depends_on = None def upgrade() -> None: # session_branches op.create_table( "session_branches", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("session_id", UUID(as_uuid=True), sa.ForeignKey("ai_sessions.id", ondelete="CASCADE"), nullable=False), sa.Column("parent_branch_id", UUID(as_uuid=True), sa.ForeignKey("session_branches.id", ondelete="CASCADE"), nullable=True), sa.Column("fork_point_step_id", UUID(as_uuid=True), sa.ForeignKey("ai_session_steps.id", ondelete="SET NULL"), nullable=True), sa.Column("branch_order", sa.Integer, nullable=False, server_default="1"), sa.Column("label", sa.String(200), nullable=False), sa.Column("status", sa.String(20), nullable=False, server_default="active"), sa.Column("status_reason", sa.Text, nullable=True), sa.Column("status_changed_at", sa.DateTime(timezone=True), nullable=True), sa.Column("status_changed_by", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True), sa.Column("conversation_messages", JSONB, nullable=False, server_default="[]"), sa.Column("context_summary", JSONB, nullable=True), sa.Column("evidence_from_branch_id", UUID(as_uuid=True), sa.ForeignKey("session_branches.id", ondelete="SET NULL"), nullable=True), sa.Column("evidence_description", sa.Text, nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.CheckConstraint("status IN ('active', 'dead_end', 'solved', 'untried', 'revived')", name="ck_session_branches_status"), sa.CheckConstraint("branch_order > 0", name="ck_session_branches_branch_order_positive"), ) op.create_index("ix_session_branches_session_id", "session_branches", ["session_id"]) op.create_index("ix_session_branches_parent_branch_id", "session_branches", ["parent_branch_id"]) op.create_index("ix_session_branches_session_status", "session_branches", ["session_id", "status"]) op.create_index("ix_session_branches_session_order", "session_branches", ["session_id", "branch_order"]) # fork_points op.create_table( "fork_points", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("session_id", UUID(as_uuid=True), sa.ForeignKey("ai_sessions.id", ondelete="CASCADE"), nullable=False), sa.Column("parent_branch_id", UUID(as_uuid=True), sa.ForeignKey("session_branches.id", ondelete="CASCADE"), nullable=False), sa.Column("trigger_step_id", UUID(as_uuid=True), sa.ForeignKey("ai_session_steps.id", ondelete="SET NULL"), nullable=True), sa.Column("fork_reason", sa.Text, nullable=False), sa.Column("options", JSONB, nullable=False, server_default="[]"), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), ) op.create_index("ix_fork_points_session_id", "fork_points", ["session_id"]) # session_handoffs op.create_table( "session_handoffs", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("session_id", UUID(as_uuid=True), sa.ForeignKey("ai_sessions.id", ondelete="CASCADE"), nullable=False), sa.Column("handed_off_by", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False), sa.Column("intent", sa.String(20), nullable=False), sa.Column("source_branch_id", UUID(as_uuid=True), sa.ForeignKey("session_branches.id", ondelete="SET NULL"), nullable=True), sa.Column("snapshot", JSONB, nullable=False, server_default="{}"), sa.Column("ai_assessment", sa.Text, nullable=True), sa.Column("ai_assessment_data", JSONB, nullable=True), sa.Column("artifacts", JSONB, nullable=True), sa.Column("engineer_notes", sa.Text, nullable=True), sa.Column("priority", sa.String(20), nullable=False, server_default="normal"), sa.Column("claimed_by", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True), sa.Column("claimed_at", sa.DateTime(timezone=True), nullable=True), sa.Column("psa_note_pushed", sa.Boolean, server_default="false"), sa.Column("psa_note_id", sa.String(100), nullable=True), sa.Column("notification_sent", sa.Boolean, server_default="false"), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.CheckConstraint("intent IN ('park', 'escalate')", name="ck_session_handoffs_intent"), sa.CheckConstraint("priority IN ('normal', 'elevated')", name="ck_session_handoffs_priority"), ) op.create_index("ix_session_handoffs_session_id", "session_handoffs", ["session_id"]) # session_resolution_outputs op.create_table( "session_resolution_outputs", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("session_id", UUID(as_uuid=True), sa.ForeignKey("ai_sessions.id", ondelete="CASCADE"), nullable=False), sa.Column("output_type", sa.String(30), nullable=False), sa.Column("generated_content", sa.Text, nullable=False), sa.Column("structured_data", JSONB, nullable=True), sa.Column("edited_content", sa.Text, nullable=True), sa.Column("status", sa.String(20), nullable=False, server_default="draft"), sa.Column("pushed_to", sa.String(50), nullable=True), sa.Column("pushed_at", sa.DateTime(timezone=True), nullable=True), sa.Column("pushed_reference", sa.String(200), nullable=True), sa.Column("generated_by_model", sa.String(50), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.CheckConstraint("output_type IN ('psa_ticket_notes', 'knowledge_base', 'client_summary')", name="ck_session_resolution_outputs_output_type"), sa.CheckConstraint("status IN ('draft', 'approved', 'pushed', 'rejected')", name="ck_session_resolution_outputs_status"), sa.UniqueConstraint("session_id", "output_type", name="uq_session_resolution_session_type"), ) op.create_index("ix_session_resolution_outputs_session_id", "session_resolution_outputs", ["session_id"]) # ai_sessions: add 5 columns (NO FK on active_branch_id) op.add_column("ai_sessions", sa.Column("is_branching", sa.Boolean, server_default="false", nullable=False)) op.add_column("ai_sessions", sa.Column("active_branch_id", UUID(as_uuid=True), nullable=True)) op.add_column("ai_sessions", sa.Column("handoff_count", sa.Integer, server_default="0", nullable=False)) op.add_column("ai_sessions", sa.Column("total_active_seconds", sa.Integer, server_default="0", nullable=False)) op.add_column("ai_sessions", sa.Column("total_parked_seconds", sa.Integer, server_default="0", nullable=False)) # ai_session_steps: add 3 columns + update CHECK op.add_column("ai_session_steps", sa.Column("branch_id", UUID(as_uuid=True), sa.ForeignKey("session_branches.id", ondelete="SET NULL"), nullable=True)) op.add_column("ai_session_steps", sa.Column("is_fork_point", sa.Boolean, server_default="false", nullable=False)) op.add_column("ai_session_steps", sa.Column("fork_point_id", UUID(as_uuid=True), sa.ForeignKey("fork_points.id", ondelete="SET NULL"), nullable=True)) op.create_index("ix_ai_session_steps_branch_id", "ai_session_steps", ["branch_id"]) op.drop_constraint("ck_ai_session_steps_step_type", "ai_session_steps", type_="check") op.create_check_constraint( "ck_ai_session_steps_step_type", "ai_session_steps", "step_type IN ('question', 'action', 'script_generation', 'verification', 'info_request', 'note', 'intake_analysis', 'fork')", ) # file_uploads: add 5 columns op.add_column("file_uploads", sa.Column("ai_description", sa.Text, nullable=True)) op.add_column("file_uploads", sa.Column("extracted_content", sa.Text, nullable=True)) op.add_column("file_uploads", sa.Column("content_summary", sa.Text, nullable=True)) op.add_column("file_uploads", sa.Column("uploaded_on_branch_id", UUID(as_uuid=True), sa.ForeignKey("session_branches.id", ondelete="SET NULL"), nullable=True)) op.add_column("file_uploads", sa.Column("uploaded_at_step_id", UUID(as_uuid=True), sa.ForeignKey("ai_session_steps.id", ondelete="SET NULL"), nullable=True)) def downgrade() -> None: op.drop_column("file_uploads", "uploaded_at_step_id") op.drop_column("file_uploads", "uploaded_on_branch_id") op.drop_column("file_uploads", "content_summary") op.drop_column("file_uploads", "extracted_content") op.drop_column("file_uploads", "ai_description") op.drop_constraint("ck_ai_session_steps_step_type", "ai_session_steps", type_="check") op.create_check_constraint( "ck_ai_session_steps_step_type", "ai_session_steps", "step_type IN ('question', 'action', 'script_generation', 'verification', 'info_request', 'note', 'intake_analysis')", ) op.drop_index("ix_ai_session_steps_branch_id", "ai_session_steps") op.drop_column("ai_session_steps", "fork_point_id") op.drop_column("ai_session_steps", "is_fork_point") op.drop_column("ai_session_steps", "branch_id") op.drop_column("ai_sessions", "total_parked_seconds") op.drop_column("ai_sessions", "total_active_seconds") op.drop_column("ai_sessions", "handoff_count") op.drop_column("ai_sessions", "active_branch_id") op.drop_column("ai_sessions", "is_branching") op.drop_table("session_resolution_outputs") op.drop_table("session_handoffs") op.drop_table("fork_points") op.drop_table("session_branches")