Files
leocrm/app/plugins/builtins/self_improvement/models.py
T
Agent Zero fbcfbbced6
Check Cross-Plugin Imports / check (push) Has been cancelled
feat(J): Phase J Self-Improvement Plugin — controlled improvement loop
- New self_improvement plugin: models, services, routes, plugin
- 4 SQLAlchemy models: ImprovementSignal, ImprovementPattern, ImprovementProposal, ImpactMeasurement
- Migration 0132: 4 tables with RLS
- Services: collect_signals, detect_patterns, create_proposal, evaluate_proposal, request_approval, activate_proposal, rollback_proposal, measure_impact
- 11 API routes under /api/v1/improvement/
- Frontend: improvement.ts API client, ImprovementPanel.tsx in AISidebar proactive tab
- 24/24 integration tests pass
- Fix: __init__.py imports Plugin class for discover_builtins() (self_improvement + knowledge)
- Fix: automation plugin register_plugin_contributions skips when no tenant exists
2026-08-21 01:34:48 +02:00

153 lines
8.2 KiB
Python

"""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())