PMFlow/backend/alembic/versions/20260513_add_pmflow_agent_r...

251 lines
14 KiB
Python

"""add durable pmflow agent runtime records
Revision ID: 20260513_add_pmflow_agent_runtime
Revises: 20260511_seed_turn_runtime_prompt_releases
Create Date: 2026-05-13
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
revision = "20260513_add_pmflow_agent_runtime"
down_revision = "20260511_seed_turn_runtime_prompt_releases"
branch_labels = None
depends_on = None
def _json_type():
bind = op.get_bind()
if bind.dialect.name == "postgresql":
from sqlalchemy.dialects import postgresql
return postgresql.JSONB()
return sa.JSON()
def _table_exists(table_name: str) -> bool:
return sa.inspect(op.get_bind()).has_table(table_name)
def _index_exists(table_name: str, index_name: str) -> bool:
inspector = sa.inspect(op.get_bind())
return any(item.get("name") == index_name for item in inspector.get_indexes(table_name))
def _create_index_once(index_name: str, table_name: str, columns: list[str]) -> None:
if _table_exists(table_name) and not _index_exists(table_name, index_name):
op.create_index(index_name, table_name, columns)
def _drop_table_once(table_name: str) -> None:
if _table_exists(table_name):
op.drop_table(table_name)
def _base_columns() -> list[sa.Column]:
return [
sa.Column("tenant_id", sa.BigInteger(), nullable=False, server_default="1"),
sa.Column("created_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("now()")),
sa.Column("updated_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("now()")),
]
def upgrade() -> None:
json_type = _json_type()
if not _table_exists("pmflow_agent_runs"):
op.create_table(
"pmflow_agent_runs",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("public_run_id", sa.String(length=80), nullable=False),
sa.Column("user_id", sa.BigInteger(), sa.ForeignKey("users.id"), nullable=True),
sa.Column("qa_session_id", sa.BigInteger(), sa.ForeignKey("qa_sessions.id"), nullable=True),
sa.Column("workspace_scope", sa.String(length=80), nullable=True),
sa.Column("surface", sa.String(length=80), nullable=True),
sa.Column("presentation_mode", sa.String(length=40), nullable=True),
sa.Column("source_channel", sa.String(length=40), nullable=True),
sa.Column("source_ref", sa.String(length=160), nullable=True),
sa.Column("title", sa.String(length=240), nullable=True),
sa.Column("goal", sa.Text(), nullable=False, server_default=""),
sa.Column("status", sa.String(length=32), nullable=False, server_default="active"),
sa.Column("active_object_refs", json_type, nullable=True),
sa.Column("current_step_id", sa.String(length=80), nullable=True),
sa.Column("final_answer", sa.Text(), nullable=True),
sa.Column("last_error", sa.Text(), nullable=True),
sa.Column("run_metadata", json_type, nullable=True),
sa.Column("started_at", sa.TIMESTAMP(), nullable=True),
sa.Column("completed_at", sa.TIMESTAMP(), nullable=True),
*_base_columns(),
sa.UniqueConstraint("tenant_id", "public_run_id", name="uq_pmflow_agent_runs_tenant_public_run_id"),
)
_create_index_once("ix_pmflow_agent_runs_tenant_id", "pmflow_agent_runs", ["tenant_id"])
_create_index_once("ix_pmflow_agent_runs_public_run_id", "pmflow_agent_runs", ["public_run_id"])
_create_index_once("ix_pmflow_agent_runs_user_id", "pmflow_agent_runs", ["user_id"])
_create_index_once("ix_pmflow_agent_runs_qa_session_id", "pmflow_agent_runs", ["qa_session_id"])
_create_index_once("ix_pmflow_agent_runs_status", "pmflow_agent_runs", ["status"])
_create_index_once("ix_pmflow_agent_runs_started_at", "pmflow_agent_runs", ["started_at"])
if not _table_exists("pmflow_agent_steps"):
op.create_table(
"pmflow_agent_steps",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column(
"run_id",
sa.BigInteger(),
sa.ForeignKey("pmflow_agent_runs.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("public_step_id", sa.String(length=80), nullable=False),
sa.Column("step_index", sa.Integer(), nullable=False, server_default="0"),
sa.Column("step_kind", sa.String(length=64), nullable=False, server_default="runtime_step"),
sa.Column("status", sa.String(length=32), nullable=False, server_default="completed"),
sa.Column("input_summary", sa.Text(), nullable=True),
sa.Column("output_summary", sa.Text(), nullable=True),
sa.Column("tool_name", sa.String(length=120), nullable=True),
sa.Column("duration_ms", sa.Integer(), nullable=True),
sa.Column("step_payload", json_type, nullable=True),
sa.Column("started_at", sa.TIMESTAMP(), nullable=True),
sa.Column("completed_at", sa.TIMESTAMP(), nullable=True),
*_base_columns(),
sa.UniqueConstraint("tenant_id", "run_id", "public_step_id", name="uq_pmflow_agent_steps_tenant_run_public_step"),
)
_create_index_once("ix_pmflow_agent_steps_tenant_id", "pmflow_agent_steps", ["tenant_id"])
_create_index_once("ix_pmflow_agent_steps_run_id", "pmflow_agent_steps", ["run_id"])
_create_index_once("ix_pmflow_agent_steps_public_step_id", "pmflow_agent_steps", ["public_step_id"])
_create_index_once("ix_pmflow_agent_steps_step_index", "pmflow_agent_steps", ["step_index"])
_create_index_once("ix_pmflow_agent_steps_step_kind", "pmflow_agent_steps", ["step_kind"])
_create_index_once("ix_pmflow_agent_steps_status", "pmflow_agent_steps", ["status"])
_create_index_once("ix_pmflow_agent_steps_tool_name", "pmflow_agent_steps", ["tool_name"])
if not _table_exists("pmflow_agent_observations"):
op.create_table(
"pmflow_agent_observations",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column(
"run_id",
sa.BigInteger(),
sa.ForeignKey("pmflow_agent_runs.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("step_id", sa.BigInteger(), sa.ForeignKey("pmflow_agent_steps.id", ondelete="SET NULL"), nullable=True),
sa.Column("public_observation_id", sa.String(length=80), nullable=False),
sa.Column("source_kind", sa.String(length=80), nullable=False, server_default="agent_runtime"),
sa.Column("customer_safe_summary", sa.Text(), nullable=True),
sa.Column("confidence", sa.String(length=32), nullable=True),
sa.Column("facts", json_type, nullable=True),
sa.Column("gaps", json_type, nullable=True),
sa.Column("raw_payload", json_type, nullable=True),
sa.Column("observed_at", sa.TIMESTAMP(), nullable=True),
*_base_columns(),
sa.UniqueConstraint(
"tenant_id",
"run_id",
"public_observation_id",
name="uq_pmflow_agent_observations_tenant_run_public_observation",
),
)
_create_index_once("ix_pmflow_agent_observations_tenant_id", "pmflow_agent_observations", ["tenant_id"])
_create_index_once("ix_pmflow_agent_observations_run_id", "pmflow_agent_observations", ["run_id"])
_create_index_once("ix_pmflow_agent_observations_step_id", "pmflow_agent_observations", ["step_id"])
_create_index_once("ix_pmflow_agent_observations_public_observation_id", "pmflow_agent_observations", ["public_observation_id"])
_create_index_once("ix_pmflow_agent_observations_source_kind", "pmflow_agent_observations", ["source_kind"])
_create_index_once("ix_pmflow_agent_observations_observed_at", "pmflow_agent_observations", ["observed_at"])
if not _table_exists("pmflow_agent_artifacts"):
op.create_table(
"pmflow_agent_artifacts",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column(
"run_id",
sa.BigInteger(),
sa.ForeignKey("pmflow_agent_runs.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("step_id", sa.BigInteger(), sa.ForeignKey("pmflow_agent_steps.id", ondelete="SET NULL"), nullable=True),
sa.Column("public_artifact_id", sa.String(length=80), nullable=False),
sa.Column("artifact_kind", sa.String(length=80), nullable=False),
sa.Column("title", sa.String(length=240), nullable=True),
sa.Column("status", sa.String(length=32), nullable=False, server_default="created"),
sa.Column("payload", json_type, nullable=True),
sa.Column("customer_safe_payload", json_type, nullable=True),
sa.Column("created_by_step_id", sa.String(length=80), nullable=True),
*_base_columns(),
sa.UniqueConstraint("tenant_id", "run_id", "public_artifact_id", name="uq_pmflow_agent_artifacts_tenant_run_public_artifact"),
)
_create_index_once("ix_pmflow_agent_artifacts_tenant_id", "pmflow_agent_artifacts", ["tenant_id"])
_create_index_once("ix_pmflow_agent_artifacts_run_id", "pmflow_agent_artifacts", ["run_id"])
_create_index_once("ix_pmflow_agent_artifacts_step_id", "pmflow_agent_artifacts", ["step_id"])
_create_index_once("ix_pmflow_agent_artifacts_public_artifact_id", "pmflow_agent_artifacts", ["public_artifact_id"])
_create_index_once("ix_pmflow_agent_artifacts_artifact_kind", "pmflow_agent_artifacts", ["artifact_kind"])
_create_index_once("ix_pmflow_agent_artifacts_status", "pmflow_agent_artifacts", ["status"])
_create_index_once("ix_pmflow_agent_artifacts_created_by_step_id", "pmflow_agent_artifacts", ["created_by_step_id"])
if not _table_exists("pmflow_agent_verifications"):
op.create_table(
"pmflow_agent_verifications",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column(
"run_id",
sa.BigInteger(),
sa.ForeignKey("pmflow_agent_runs.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("step_id", sa.BigInteger(), sa.ForeignKey("pmflow_agent_steps.id", ondelete="SET NULL"), nullable=True),
sa.Column("public_verification_id", sa.String(length=80), nullable=False),
sa.Column("target_ref", sa.String(length=160), nullable=True),
sa.Column("result", sa.String(length=32), nullable=False, server_default="unknown"),
sa.Column("retry_would_change_state", sa.Boolean(), nullable=False, server_default=sa.text("false")),
sa.Column("findings", json_type, nullable=True),
sa.Column("verification_payload", json_type, nullable=True),
sa.Column("verified_at", sa.TIMESTAMP(), nullable=True),
*_base_columns(),
sa.UniqueConstraint(
"tenant_id",
"run_id",
"public_verification_id",
name="uq_pmflow_agent_verifications_tenant_run_public_verification",
),
)
_create_index_once("ix_pmflow_agent_verifications_tenant_id", "pmflow_agent_verifications", ["tenant_id"])
_create_index_once("ix_pmflow_agent_verifications_run_id", "pmflow_agent_verifications", ["run_id"])
_create_index_once("ix_pmflow_agent_verifications_step_id", "pmflow_agent_verifications", ["step_id"])
_create_index_once("ix_pmflow_agent_verifications_public_verification_id", "pmflow_agent_verifications", ["public_verification_id"])
_create_index_once("ix_pmflow_agent_verifications_target_ref", "pmflow_agent_verifications", ["target_ref"])
_create_index_once("ix_pmflow_agent_verifications_result", "pmflow_agent_verifications", ["result"])
_create_index_once("ix_pmflow_agent_verifications_verified_at", "pmflow_agent_verifications", ["verified_at"])
if not _table_exists("pmflow_agent_memories"):
op.create_table(
"pmflow_agent_memories",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column(
"run_id",
sa.BigInteger(),
sa.ForeignKey("pmflow_agent_runs.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("memory_kind", sa.String(length=80), nullable=False),
sa.Column("scope_ref", sa.String(length=160), nullable=True),
sa.Column("outcome_signal", sa.String(length=80), nullable=True),
sa.Column("content", json_type, nullable=True),
sa.Column("memory_payload", json_type, nullable=True),
*_base_columns(),
)
_create_index_once("ix_pmflow_agent_memories_tenant_id", "pmflow_agent_memories", ["tenant_id"])
_create_index_once("ix_pmflow_agent_memories_run_id", "pmflow_agent_memories", ["run_id"])
_create_index_once("ix_pmflow_agent_memories_memory_kind", "pmflow_agent_memories", ["memory_kind"])
_create_index_once("ix_pmflow_agent_memories_scope_ref", "pmflow_agent_memories", ["scope_ref"])
_create_index_once("ix_pmflow_agent_memories_outcome_signal", "pmflow_agent_memories", ["outcome_signal"])
def downgrade() -> None:
_drop_table_once("pmflow_agent_memories")
_drop_table_once("pmflow_agent_verifications")
_drop_table_once("pmflow_agent_artifacts")
_drop_table_once("pmflow_agent_observations")
_drop_table_once("pmflow_agent_steps")
_drop_table_once("pmflow_agent_runs")