"""Self-improvement models — signals, patterns, proposals, impact measurements. All models are real SQLAlchemy models with TenantMixin (no dataclasses). Tables get RLS via migration. """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import ( DateTime, Float, ForeignKey, Index, Integer, String, Text, func, ) from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID from sqlalchemy.orm import Mapped, mapped_column from app.core.db import Base, TenantMixin from app.models.owned_mixin import OwnedMixin # Constants SIGNAL_KINDS = ("failure", "retry", "dismissal", "correction", "observation", "handoff") SEVERITY_LEVELS = ("info", "warning", "error") PATTERN_KINDS = ("retry_bottleneck", "manual_correction", "suggestion_dismissal", "repetitive_handoff") PATTERN_STATUSES = ("detected", "proposal_created", "resolved", "ignored") PROPOSAL_STATUSES = ( "draft", "evaluating", "evaluated", "pending_approval", "approved", "active", "rolled_back", "rejected", "expired", ) PROPOSAL_TARGET_TYPES = ("agent", "skill", "trigger", "workflow", "miniapp_template", "plugin_patch") class ImprovementSignal(Base, TenantMixin): """A single improvement signal collected from real system usage. References source data (AgentRun, WorkflowInstance, ProactiveSuggestion, AuditLog) by ID — stores no personal data copies. """ __tablename__ = "improvement_signals" __table_args__ = ( Index("ix_impr_signals_tenant_kind", "tenant_id", "signal_kind"), Index("ix_impr_signals_tenant_source", "tenant_id", "source_type"), Index("ix_impr_signals_tenant_pattern", "tenant_id", "pattern_id"), ) id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4) source_type: Mapped[str] = mapped_column(String(50), nullable=False) source_ref_id: Mapped[uuid.UUID | None] = mapped_column(PGUUID(as_uuid=True), nullable=True) source_metadata: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) summary: Mapped[str] = mapped_column(Text, nullable=False) signal_kind: Mapped[str] = mapped_column(String(30), nullable=False) severity: Mapped[str] = mapped_column(String(20), nullable=False, default="info") confidence: Mapped[float] = mapped_column(Float, nullable=False, default=0.5) pattern_id: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("improvement_patterns.id", ondelete="SET NULL"), nullable=True ) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) class ImprovementPattern(Base, TenantMixin): """A detected pattern from grouped improvement signals.""" __tablename__ = "improvement_patterns" __table_args__ = ( Index("ix_impr_patterns_tenant_status", "tenant_id", "status"), Index("ix_impr_patterns_tenant_target", "tenant_id", "target_type"), ) id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4) pattern_kind: Mapped[str] = mapped_column(String(40), nullable=False) title: Mapped[str] = mapped_column(String(300), nullable=False) description: Mapped[str] = mapped_column(Text, nullable=False, default="") target_type: Mapped[str] = mapped_column(String(30), nullable=False) target_name: Mapped[str] = mapped_column(String(200), nullable=False, default="unknown") evidence_refs: Mapped[list] = mapped_column(JSONB, nullable=False, default=list) occurrence_count: Mapped[int] = mapped_column(Integer, nullable=False, default=1) confidence: Mapped[float] = mapped_column(Float, nullable=False, default=0.5) status: Mapped[str] = mapped_column(String(20), nullable=False, default="detected") proposed_action: Mapped[str] = mapped_column(Text, nullable=False, default="") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()) class ImprovementProposal(Base, TenantMixin, OwnedMixin): """A versioned improvement proposal with lifecycle. Status flow: draft -> evaluating -> evaluated -> pending_approval -> approved -> active -> rolled_back rejected <- pending_approval expired <- pending_approval """ __tablename__ = "improvement_proposals" __table_args__ = ( Index("ix_impr_proposals_tenant_status", "tenant_id", "status"), Index("ix_impr_proposals_tenant_target", "tenant_id", "target_type"), Index("ix_impr_proposals_tenant_pattern", "tenant_id", "pattern_id"), ) id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4) pattern_id: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("improvement_patterns.id", ondelete="SET NULL"), nullable=True ) title: Mapped[str] = mapped_column(String(300), nullable=False) description: Mapped[str] = mapped_column(Text, nullable=False, default="") target_type: Mapped[str] = mapped_column(String(30), nullable=False) target_ref_id: Mapped[uuid.UUID | None] = mapped_column(PGUUID(as_uuid=True), nullable=True) target_name: Mapped[str | None] = mapped_column(String(200), nullable=True) version_number: Mapped[int] = mapped_column(Integer, nullable=False, default=1) proposed_config: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) previous_config: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) evidence_refs: Mapped[list] = mapped_column(JSONB, nullable=False, default=list) rationale: Mapped[str] = mapped_column(Text, nullable=False, default="") expected_benefit: Mapped[str] = mapped_column(Text, nullable=False, default="") risk_assessment: Mapped[str] = mapped_column(Text, nullable=False, default="") evaluation_result: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) evaluated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) approval_request_id: Mapped[uuid.UUID | None] = mapped_column(PGUUID(as_uuid=True), nullable=True) approved_by: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True ) approved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) activated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) rolled_back_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) rollback_reason: Mapped[str | None] = mapped_column(Text, nullable=True) status: Mapped[str] = mapped_column(String(20), nullable=False, default="draft") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()) class ImpactMeasurement(Base, TenantMixin): """Pre/post impact measurement for an activated proposal.""" __tablename__ = "improvement_impact_measurements" __table_args__ = ( Index("ix_impr_impact_tenant_proposal", "tenant_id", "proposal_id"), ) id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4) proposal_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("improvement_proposals.id", ondelete="CASCADE"), nullable=False ) pre_metrics: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) post_metrics: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) delta: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) assessment: Mapped[str] = mapped_column(Text, nullable=False, default="") is_positive: Mapped[str] = mapped_column(String(20), nullable=False, default="neutral") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())