feat(l1): add ai_build session kind (model + migration)
Teaches l1_walk_sessions a new session_kind='ai_build' for AI-generated decision-tree walks. FK shape matches adhoc: both flow_id and flow_proposal_id must be NULL. Drops and recreates the two affected CHECK constraints (session_kind allowlist + target_consistency). Migration beca7464b6b4 chains from b3358ba0e48c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
"""add ai_build session kind
|
||||||
|
|
||||||
|
Revision ID: beca7464b6b4
|
||||||
|
Revises: b3358ba0e48c
|
||||||
|
Create Date: 2026-05-29 18:41:38.601537
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'beca7464b6b4'
|
||||||
|
down_revision: Union[str, None] = 'b3358ba0e48c'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.drop_constraint("ck_l1_walk_sessions_session_kind", "l1_walk_sessions", type_="check")
|
||||||
|
op.create_check_constraint(
|
||||||
|
"ck_l1_walk_sessions_session_kind", "l1_walk_sessions",
|
||||||
|
"session_kind IN ('flow', 'proposal', 'adhoc', 'ai_build')",
|
||||||
|
)
|
||||||
|
op.drop_constraint("ck_l1_walk_sessions_target_consistency", "l1_walk_sessions", type_="check")
|
||||||
|
op.create_check_constraint(
|
||||||
|
"ck_l1_walk_sessions_target_consistency", "l1_walk_sessions",
|
||||||
|
"(session_kind = 'flow' AND flow_id IS NOT NULL AND flow_proposal_id IS NULL) "
|
||||||
|
"OR (session_kind = 'proposal' AND flow_proposal_id IS NOT NULL AND flow_id IS NULL) "
|
||||||
|
"OR (session_kind IN ('adhoc', 'ai_build') AND flow_id IS NULL AND flow_proposal_id IS NULL)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_constraint("ck_l1_walk_sessions_target_consistency", "l1_walk_sessions", type_="check")
|
||||||
|
op.create_check_constraint(
|
||||||
|
"ck_l1_walk_sessions_target_consistency", "l1_walk_sessions",
|
||||||
|
"(session_kind = 'flow' AND flow_id IS NOT NULL AND flow_proposal_id IS NULL) "
|
||||||
|
"OR (session_kind = 'proposal' AND flow_proposal_id IS NOT NULL AND flow_id IS NULL) "
|
||||||
|
"OR (session_kind = 'adhoc' AND flow_id IS NULL AND flow_proposal_id IS NULL)",
|
||||||
|
)
|
||||||
|
op.drop_constraint("ck_l1_walk_sessions_session_kind", "l1_walk_sessions", type_="check")
|
||||||
|
op.create_check_constraint(
|
||||||
|
"ck_l1_walk_sessions_session_kind", "l1_walk_sessions",
|
||||||
|
"session_kind IN ('flow', 'proposal', 'adhoc')",
|
||||||
|
)
|
||||||
@@ -30,6 +30,7 @@ class L1WalkSession(Base):
|
|||||||
- flow: Walking a published flow (flow_id required, flow_proposal_id null).
|
- flow: Walking a published flow (flow_id required, flow_proposal_id null).
|
||||||
- proposal: Walking a draft flow proposal (flow_proposal_id required, flow_id null).
|
- proposal: Walking a draft flow proposal (flow_proposal_id required, flow_id null).
|
||||||
- adhoc: Free-form investigation (both flow_id and flow_proposal_id null).
|
- adhoc: Free-form investigation (both flow_id and flow_proposal_id null).
|
||||||
|
- ai_build: AI-generated decision-tree walk (both flow_id and flow_proposal_id null).
|
||||||
|
|
||||||
status lifecycle:
|
status lifecycle:
|
||||||
- active: Session is in progress.
|
- active: Session is in progress.
|
||||||
@@ -45,7 +46,7 @@ class L1WalkSession(Base):
|
|||||||
name="ck_l1_walk_sessions_ticket_kind",
|
name="ck_l1_walk_sessions_ticket_kind",
|
||||||
),
|
),
|
||||||
CheckConstraint(
|
CheckConstraint(
|
||||||
"session_kind IN ('flow', 'proposal', 'adhoc')",
|
"session_kind IN ('flow', 'proposal', 'adhoc', 'ai_build')",
|
||||||
name="ck_l1_walk_sessions_session_kind",
|
name="ck_l1_walk_sessions_session_kind",
|
||||||
),
|
),
|
||||||
CheckConstraint(
|
CheckConstraint(
|
||||||
@@ -55,7 +56,7 @@ class L1WalkSession(Base):
|
|||||||
CheckConstraint(
|
CheckConstraint(
|
||||||
"(session_kind = 'flow' AND flow_id IS NOT NULL AND flow_proposal_id IS NULL) "
|
"(session_kind = 'flow' AND flow_id IS NOT NULL AND flow_proposal_id IS NULL) "
|
||||||
"OR (session_kind = 'proposal' AND flow_proposal_id IS NOT NULL AND flow_id IS NULL) "
|
"OR (session_kind = 'proposal' AND flow_proposal_id IS NOT NULL AND flow_id IS NULL) "
|
||||||
"OR (session_kind = 'adhoc' AND flow_id IS NULL AND flow_proposal_id IS NULL)",
|
"OR (session_kind IN ('adhoc', 'ai_build') AND flow_id IS NULL AND flow_proposal_id IS NULL)",
|
||||||
name="ck_l1_walk_sessions_target_consistency",
|
name="ck_l1_walk_sessions_target_consistency",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
16
backend/tests/test_l1_ai_build_model.py
Normal file
16
backend/tests/test_l1_ai_build_model.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from app.models.l1_walk_session import L1WalkSession
|
||||||
|
|
||||||
|
|
||||||
|
def test_ai_build_session_kind_allowed_by_model_constraint():
|
||||||
|
"""ai_build is a valid session_kind with both target FKs null (like adhoc)."""
|
||||||
|
s = L1WalkSession(
|
||||||
|
account_id=uuid.uuid4(),
|
||||||
|
created_by_user_id=uuid.uuid4(),
|
||||||
|
ticket_id="t1",
|
||||||
|
ticket_kind="internal",
|
||||||
|
session_kind="ai_build",
|
||||||
|
)
|
||||||
|
assert s.session_kind == "ai_build"
|
||||||
|
assert s.flow_id is None and s.flow_proposal_id is None
|
||||||
Reference in New Issue
Block a user