"""add mounted ai runtime tables Revision ID: 20260405_add_mounted_ai_runtime_tables Revises: 20260331_add_qa_record_runtime_fields Create Date: 2026-04-05 """ from __future__ import annotations from alembic import op import sqlalchemy as sa try: from sqlalchemy.dialects.postgresql import JSONB except Exception: # pragma: no cover JSONB = sa.JSON revision = "20260405_add_mounted_ai_runtime_tables" down_revision = "20260331_add_qa_record_runtime_fields" branch_labels = None depends_on = None def _json_type(bind): return JSONB() if bind.dialect.name == "postgresql" else sa.JSON() def _table_exists(bind, table_name: str) -> bool: return sa.inspect(bind).has_table(table_name) def _index_exists(bind, table_name: str, index_name: str) -> bool: inspector = sa.inspect(bind) try: indexes = inspector.get_indexes(table_name) except Exception: return False return any(str(index.get("name")) == index_name for index in indexes) def _create_index_if_missing(bind, table_name: str, index_name: str, columns: list[str], *, unique: bool = False) -> None: if not _index_exists(bind, table_name, index_name): op.create_index(index_name, table_name, columns, unique=unique) def _base_columns() -> list[sa.Column]: return [ sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), nullable=False, server_default=sa.func.now()), sa.Column("updated_at", sa.TIMESTAMP(), nullable=False, server_default=sa.func.now()), ] def _create_mounted_topic_workspaces(bind) -> None: if _table_exists(bind, "mounted_topic_workspaces"): return op.create_table( "mounted_topic_workspaces", sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("topic_key", sa.VARCHAR(length=128), nullable=False), sa.Column("source_system", sa.VARCHAR(length=32), nullable=False), sa.Column("channel_ref", sa.VARCHAR(length=128), nullable=True), sa.Column("thread_key", sa.VARCHAR(length=255), nullable=True), sa.Column("root_message_id", sa.VARCHAR(length=128), nullable=True), sa.Column("mode", sa.VARCHAR(length=24), nullable=False, server_default="chat"), sa.Column("status", sa.VARCHAR(length=24), nullable=False, server_default="active"), sa.Column("facts_json", _json_type(bind), nullable=True), sa.Column("goals_json", _json_type(bind), nullable=True), sa.Column("constraints_json", _json_type(bind), nullable=True), sa.Column("options_json", _json_type(bind), nullable=True), sa.Column("open_questions", _json_type(bind), nullable=True), sa.Column("pending_actions", _json_type(bind), nullable=True), sa.Column("bound_entities", _json_type(bind), nullable=True), sa.Column("readiness_score", sa.Float(), nullable=True, server_default="0"), sa.Column("last_message_id", sa.VARCHAR(length=128), nullable=True), sa.Column("last_triggered_at", sa.TIMESTAMP(), nullable=True), sa.Column("metadata_json", _json_type(bind), nullable=True), *_base_columns(), sa.UniqueConstraint( "tenant_id", "source_system", "topic_key", name="uq_mounted_topic_workspace_source_topic", ), ) for index_name, columns in ( ("ix_mounted_topic_workspaces_tenant_id", ["tenant_id"]), ("ix_mounted_topic_workspaces_topic_key", ["topic_key"]), ("ix_mounted_topic_workspaces_source_system", ["source_system"]), ("ix_mounted_topic_workspaces_channel_ref", ["channel_ref"]), ("ix_mounted_topic_workspaces_thread_key", ["thread_key"]), ("ix_mounted_topic_workspaces_root_message_id", ["root_message_id"]), ("ix_mounted_topic_workspaces_mode", ["mode"]), ("ix_mounted_topic_workspaces_status", ["status"]), ("ix_mounted_topic_workspaces_last_message_id", ["last_message_id"]), ): _create_index_if_missing(bind, "mounted_topic_workspaces", index_name, columns) def _create_mounted_draft_workspaces(bind) -> None: if _table_exists(bind, "mounted_draft_workspaces"): return op.create_table( "mounted_draft_workspaces", sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column( "topic_workspace_id", sa.Integer(), sa.ForeignKey("mounted_topic_workspaces.id", ondelete="CASCADE"), nullable=False, ), sa.Column("state", sa.VARCHAR(length=24), nullable=False, server_default="collecting"), sa.Column("draft_version", sa.Integer(), nullable=False, server_default="0"), sa.Column("requirement_card", _json_type(bind), nullable=True), sa.Column("draft_payload", _json_type(bind), nullable=True), sa.Column("required_confirmations", _json_type(bind), nullable=True), sa.Column("last_ready_at", sa.TIMESTAMP(), nullable=True), sa.Column("confirm_message_id", sa.VARCHAR(length=128), nullable=True), *_base_columns(), sa.UniqueConstraint( "tenant_id", "topic_workspace_id", name="uq_mounted_draft_workspace_topic", ), ) for index_name, columns in ( ("ix_mounted_draft_workspaces_tenant_id", ["tenant_id"]), ("ix_mounted_draft_workspaces_topic_workspace_id", ["topic_workspace_id"]), ("ix_mounted_draft_workspaces_state", ["state"]), ("ix_mounted_draft_workspaces_confirm_message_id", ["confirm_message_id"]), ): _create_index_if_missing(bind, "mounted_draft_workspaces", index_name, columns) def _create_mounted_conversation_runs(bind) -> None: if _table_exists(bind, "mounted_conversation_runs"): return op.create_table( "mounted_conversation_runs", sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("trace_id", sa.VARCHAR(length=64), nullable=False), sa.Column( "topic_workspace_id", sa.Integer(), sa.ForeignKey("mounted_topic_workspaces.id", ondelete="SET NULL"), nullable=True, ), sa.Column("source_system", sa.VARCHAR(length=32), nullable=False), sa.Column("source_message_id", sa.VARCHAR(length=128), nullable=True), sa.Column("mode_before", sa.VARCHAR(length=24), nullable=True), sa.Column("mode_after", sa.VARCHAR(length=24), nullable=True), sa.Column("active_agent", sa.VARCHAR(length=64), nullable=True), sa.Column("status", sa.VARCHAR(length=24), nullable=False, server_default="queued"), sa.Column("trigger_reason", sa.VARCHAR(length=128), nullable=True), sa.Column("context_layers_hit", _json_type(bind), nullable=True), sa.Column("tool_calls", _json_type(bind), nullable=True), sa.Column("final_reply", sa.Text(), nullable=True), sa.Column("execution_outcome", _json_type(bind), nullable=True), sa.Column("error_text", sa.Text(), nullable=True), sa.Column("started_at", sa.TIMESTAMP(), nullable=True), sa.Column("completed_at", sa.TIMESTAMP(), nullable=True), *_base_columns(), sa.UniqueConstraint("tenant_id", "trace_id", name="uq_mounted_conversation_run_trace"), ) for index_name, columns in ( ("ix_mounted_conversation_runs_tenant_id", ["tenant_id"]), ("ix_mounted_conversation_runs_trace_id", ["trace_id"]), ("ix_mounted_conversation_runs_topic_workspace_id", ["topic_workspace_id"]), ("ix_mounted_conversation_runs_source_system", ["source_system"]), ("ix_mounted_conversation_runs_source_message_id", ["source_message_id"]), ("ix_mounted_conversation_runs_mode_before", ["mode_before"]), ("ix_mounted_conversation_runs_mode_after", ["mode_after"]), ("ix_mounted_conversation_runs_active_agent", ["active_agent"]), ("ix_mounted_conversation_runs_status", ["status"]), ("ix_mounted_conversation_runs_trigger_reason", ["trigger_reason"]), ): _create_index_if_missing(bind, "mounted_conversation_runs", index_name, columns) def _create_mounted_agent_traces(bind) -> None: if _table_exists(bind, "mounted_agent_traces"): return op.create_table( "mounted_agent_traces", sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column( "conversation_run_id", sa.Integer(), sa.ForeignKey("mounted_conversation_runs.id", ondelete="CASCADE"), nullable=False, ), sa.Column("agent_name", sa.VARCHAR(length=64), nullable=False), sa.Column("status", sa.VARCHAR(length=24), nullable=False, server_default="completed"), sa.Column("latency_ms", sa.Integer(), nullable=True), sa.Column("input_summary", _json_type(bind), nullable=True), sa.Column("output_summary", _json_type(bind), nullable=True), sa.Column("tool_trace", _json_type(bind), nullable=True), sa.Column("failure_reason", sa.Text(), nullable=True), *_base_columns(), ) for index_name, columns in ( ("ix_mounted_agent_traces_tenant_id", ["tenant_id"]), ("ix_mounted_agent_traces_conversation_run_id", ["conversation_run_id"]), ("ix_mounted_agent_traces_agent_name", ["agent_name"]), ("ix_mounted_agent_traces_status", ["status"]), ): _create_index_if_missing(bind, "mounted_agent_traces", index_name, columns) def _create_mounted_tool_capability_policies(bind) -> None: if _table_exists(bind, "mounted_tool_capability_policies"): return op.create_table( "mounted_tool_capability_policies", sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tool_name", sa.VARCHAR(length=128), nullable=False), sa.Column("risk_level", sa.VARCHAR(length=24), nullable=False, server_default="medium"), sa.Column("confirm_required", sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column("auto_execute_allowed", sa.Boolean(), nullable=False, server_default=sa.false()), sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column("source_scope", _json_type(bind), nullable=True), sa.Column("tenant_scope", _json_type(bind), nullable=True), sa.Column("policy_metadata", _json_type(bind), nullable=True), sa.Column("notes", sa.Text(), nullable=True), *_base_columns(), sa.UniqueConstraint( "tenant_id", "tool_name", name="uq_mounted_tool_capability_policy_tool", ), ) for index_name, columns in ( ("ix_mounted_tool_capability_policies_tenant_id", ["tenant_id"]), ("ix_mounted_tool_capability_policies_tool_name", ["tool_name"]), ("ix_mounted_tool_capability_policies_risk_level", ["risk_level"]), ("ix_mounted_tool_capability_policies_is_active", ["is_active"]), ): _create_index_if_missing(bind, "mounted_tool_capability_policies", index_name, columns) def upgrade() -> None: bind = op.get_bind() _create_mounted_topic_workspaces(bind) _create_mounted_draft_workspaces(bind) _create_mounted_conversation_runs(bind) _create_mounted_agent_traces(bind) _create_mounted_tool_capability_policies(bind) def downgrade() -> None: bind = op.get_bind() for table_name in ( "mounted_tool_capability_policies", "mounted_agent_traces", "mounted_conversation_runs", "mounted_draft_workspaces", "mounted_topic_workspaces", ): if _table_exists(bind, table_name): op.drop_table(table_name)