Files
resolutionflow/backend/app/models/flow_proposal.py
Michael Chihlas c576c6609e feat(l1): extend FlowProposal with source/linked_ticket/validated_by_outcome
Adds source (NOT NULL, backfilled to 'manual_draft'), linked_ticket_id,
linked_ticket_kind, validated_by_outcome columns. CHECK constraints on
source values and linked_ticket_kind values. walked_path lives on the
new l1_walk_sessions table (Task 6) — NOT on FlowProposal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:27:07 -04:00

171 lines
6.4 KiB
Python

"""Flow proposal model.
Generated by the Knowledge Flywheel after AI sessions resolve.
Represents a proposed new flow or enhancement awaiting human review.
"""
import uuid
from datetime import datetime, timezone
from typing import Optional, Any, TYPE_CHECKING
from sqlalchemy import String, Text, DateTime, ForeignKey, Integer, Float, Boolean, CheckConstraint, text as sa_text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID, JSONB
from app.core.database import Base
if TYPE_CHECKING:
from app.models.user import User
from app.models.team import Team
from app.models.account import Account
from app.models.tree import Tree
from app.models.ai_session import AISession
class FlowProposal(Base):
"""A proposed new flow or enhancement generated from an AI session.
proposal_type:
- new_flow: No similar flow exists. Full flow definition proposed.
- enhancement: Similar flow exists but session discovered new branch/edge case.
- branch_addition: A single new branch to add to an existing flow.
- auto_reinforced: Session matched existing flow exactly (tracking only).
status:
- pending: Awaiting review
- approved: Reviewed and published to knowledge base
- modified: Reviewer edited before publishing
- rejected: Reviewer decided not to publish (bad quality)
- dismissed: Parked for later — not wrong, just not actionable now.
- auto_reinforced: Session matched existing flow exactly (no review needed)
"""
__tablename__ = "flow_proposals"
__table_args__ = (
CheckConstraint(
"proposal_type IN ('new_flow', 'enhancement', 'branch_addition', 'auto_reinforced')",
name="ck_flow_proposals_type",
),
CheckConstraint(
"status IN ('pending', 'approved', 'modified', 'rejected', 'dismissed', 'auto_reinforced')",
name="ck_flow_proposals_status",
),
CheckConstraint(
"source IN ('ai_realtime_l1', 'kb_accelerator', 'manual_draft', 'ai_promoted')",
name="ck_flow_proposals_source",
),
CheckConstraint(
"linked_ticket_kind IS NULL OR linked_ticket_kind IN ('psa', 'internal')",
name="ck_flow_proposals_linked_ticket_kind",
),
)
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
account_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("accounts.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
team_id: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True),
ForeignKey("teams.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
source_session_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("ai_sessions.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
# ── Proposal details ──
proposal_type: Mapped[str] = mapped_column(
String(30), nullable=False,
)
target_flow_id: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True),
ForeignKey("trees.id", ondelete="SET NULL"),
nullable=True,
comment="For enhancements: which existing flow to modify",
)
title: Mapped[str] = mapped_column(
String(255), nullable=False,
comment="Human-readable title for the proposed flow",
)
description: Mapped[Optional[str]] = mapped_column(
Text, nullable=True,
comment="AI-generated description of what this flow covers",
)
proposed_flow_data: Mapped[dict[str, Any]] = mapped_column(
JSONB, nullable=False,
comment="Complete flow/tree_structure definition (nodes, edges, conditions)",
)
proposed_diff: Mapped[Optional[dict[str, Any]]] = mapped_column(
JSONB, nullable=True,
comment="For enhancements: what changed vs existing flow",
)
# ── Scoring ──
confidence_score: Mapped[float] = mapped_column(
Float, nullable=False, default=0.0,
comment="How confident the system is in this proposal (0.0-1.0)",
)
supporting_session_count: Mapped[int] = mapped_column(
Integer, nullable=False, default=1,
comment="Number of sessions with similar resolution paths",
)
supporting_session_ids: Mapped[list] = mapped_column(
JSONB, nullable=False, default=list,
comment="Array of session IDs that support this proposal",
)
problem_domain: Mapped[Optional[str]] = mapped_column(
String(100), nullable=True,
)
# ── Review ──
status: Mapped[str] = mapped_column(
String(30), nullable=False, default="pending", index=True,
)
reviewed_by: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
)
reviewer_notes: Mapped[Optional[str]] = mapped_column(
Text, nullable=True,
)
published_flow_id: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True),
ForeignKey("trees.id", ondelete="SET NULL"),
nullable=True,
comment="The flow that was created/updated when this proposal was approved",
)
# ── L1 workspace ──
source: Mapped[str] = mapped_column(
String(30), nullable=False, server_default=sa_text("'manual_draft'"),
)
linked_ticket_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
linked_ticket_kind: Mapped[Optional[str]] = mapped_column(String(10), nullable=True)
validated_by_outcome: Mapped[bool] = mapped_column(
Boolean(), nullable=False, server_default=sa_text('false'),
)
# ── Timestamps ──
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
reviewed_at: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True), nullable=True,
)
# ── Relationships ──
account: Mapped["Account"] = relationship("Account")
team: Mapped[Optional["Team"]] = relationship("Team")
source_session: Mapped["AISession"] = relationship("AISession")
target_flow: Mapped[Optional["Tree"]] = relationship("Tree", foreign_keys=[target_flow_id])
published_flow: Mapped[Optional["Tree"]] = relationship("Tree", foreign_keys=[published_flow_id])
reviewer: Mapped[Optional["User"]] = relationship("User")