from sqlalchemy import CheckConstraint, Column, Integer, String, DateTime, Text, Boolean, ForeignKey, Index, Float from sqlalchemy.orm import relationship from database.models import Base from datetime import datetime class AgentContext(Base): """ 共享上下文表(替代硬編碼鏈),支援多 Agent 存取與 TTL。 索引:(session_id, agent_name, context_key) 以加速跨 Agent 查詢。 """ __tablename__ = 'agent_context' id = Column(Integer, primary_key=True, autoincrement=True) session_id = Column(String(64), nullable=False, index=True) agent_name = Column(String(50), nullable=False, index=True) context_key = Column(String(100), nullable=False) context_val = Column(Text) # JSON string created_at = Column(DateTime, default=datetime.now) ttl_minutes = Column(Integer, default=60) __table_args__ = ( Index('idx_agent_context_session_key', 'session_id', 'agent_name', 'context_key'), Index('idx_agent_context_session_ttl', 'session_id', 'created_at'), {'extend_existing': True}, ) class ActionPlan(Base): """ 行動計畫表 — 統一 schema(超集)。 Group A(01-init.sql / CodeReview / OpenClaw): action_type, description, priority, metadata_json Group B(migration 017 / watcher_agent / ai_orchestrator): session_id, plan_type, sku, payload, created_by, approved_by, executed_at migration 019 已在 DB 補齊所有欄位。 """ __tablename__ = 'action_plans' id = Column(Integer, primary_key=True, autoincrement=True) # Group A columns action_type = Column(String(100), nullable=True) # code_review_fix / openclaw_recommendation description = Column(Text) # 人類可讀的行動說明 status = Column(String(50), default='pending') # pending/auto_pending/pending_review/executed priority = Column(Integer, default=3) # 1=critical 2=high 3=medium 4=low metadata_json = Column(Text) # JSON: pipeline_id/commit_sha/findings created_at = Column(DateTime, default=datetime.now) # Group B columns (ADR-012 / NemoTron) session_id = Column(String(64), nullable=True) plan_type = Column(String(50), nullable=True) # price_adjust / restock / campaign sku = Column(String(100), nullable=True) payload = Column(Text) # JSON 行動內容 created_by = Column(String(50)) # nemotron / openclaw approved_by = Column(String(100), nullable=True) # Telegram user_id executed_at = Column(DateTime, nullable=True) __table_args__ = ( CheckConstraint( "action_type IS NOT NULL OR created_by IS NOT NULL", name="chk_action_plans_source_marker", ), CheckConstraint( "action_type IS NULL OR action_type IN ('auto', 'code_review_fix', 'openclaw_recommendation')", name="chk_action_plans_action_type", ), CheckConstraint( "created_by IS NULL OR created_by IN (" "'nemotron', 'openclaw', 'code_review_pipeline', " "'ai_orchestrator', 'watcher_agent', 'agent_actions', " "'elephant_alpha', 'manual', 'system'" ")", name="chk_action_plans_created_by", ), CheckConstraint( "status IS NULL OR status IN (" "'pending', 'approved', 'rejected', 'executed', " "'auto_pending', 'auto_disabled', 'pending_review'" ")", name="chk_action_plans_status", ), Index('idx_action_plans_type', 'action_type'), Index('idx_action_plan_sku_status', 'sku', 'status'), Index('idx_action_plan_created', 'created_at'), {'extend_existing': True}, ) class ActionOutcome(Base): """ 行動結果追蹤(閉環學習核心)。 """ __tablename__ = 'action_outcomes' __table_args__ = {'extend_existing': True} id = Column(Integer, primary_key=True, autoincrement=True) plan_id = Column(Integer, ForeignKey('action_plans.id'), nullable=False) metric_type = Column(String(50), nullable=True) # sales_7d / price_rank / conversion before_val = Column(Float) after_val = Column(Float) measured_at = Column(DateTime) verdict = Column(String(20)) # effective / neutral / backfired created_at = Column(DateTime, default=datetime.now) plan = relationship("ActionPlan", backref="outcomes") class AgentStrategyWeights(Base): """ Agent strategy weights (OpenClaw learning accumulation). Index: strategy_key for fast updates/query. """ __tablename__ = 'agent_strategy_weights' id = Column(Integer, primary_key=True, autoincrement=True) strategy_key = Column(String(100), unique=True, nullable=False) # e.g. price_cut_when_gap_gt_5pct weight = Column(Float, default=1.0) success_cnt = Column(Integer, default=0) fail_cnt = Column(Integer, default=0) updated_at = Column(DateTime, default=datetime.now) __table_args__ = ( Index('idx_strategy_key', 'strategy_key'), {'extend_existing': True}, ) class Incident(Base): """ Incident tracking for AIOps auto-healing. """ __tablename__ = 'incidents' id = Column(Integer, primary_key=True, autoincrement=True) task_name = Column(String(100), nullable=False) error_type = Column(String(50), nullable=False) error_message = Column(Text, nullable=False) error_traceback = Column(Text) traceback_str = Column(Text) severity = Column(String(20), default='medium') status = Column(String(20), default='open') # open/healing/closed/escalated retry_count = Column(Integer, default=0) playbook_id = Column(Integer, ForeignKey('playbooks.id'), nullable=True) matched_playbook_id = Column(Integer, ForeignKey('playbooks.id'), nullable=True) resolved_at = Column(DateTime, nullable=True) created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now) # Relationship playbook = relationship("Playbook", foreign_keys=[matched_playbook_id], backref="incidents") class Playbook(Base): """ Playbook definitions for auto-healing actions. """ __tablename__ = 'playbooks' id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(100), nullable=False) description = Column(Text) error_type = Column(String(50), nullable=False) match_pattern = Column(Text) # JSON array of patterns action_type = Column(String(50), nullable=False) # DOCKER_RESTART/SSH_CMD/ALERT_ONLY/WAIT_RETRY action_params = Column(Text) # JSON params max_retries = Column(Integer, default=3) cooldown_min = Column(Integer, default=30) is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now) def get_match_patterns(self): """Parse match_pattern JSON to list""" if self.match_pattern: try: import json return json.loads(self.match_pattern) except: return [] return [] def get_action_params(self): """Parse action_params JSON to dict""" if self.action_params: try: import json return json.loads(self.action_params) except: return {} return {} class HealLog(Base): """ Healing execution logs. """ __tablename__ = 'heal_logs' id = Column(Integer, primary_key=True, autoincrement=True) incident_id = Column(Integer, ForeignKey('incidents.id'), nullable=False) playbook_id = Column(Integer, ForeignKey('playbooks.id'), nullable=False) action_type = Column(String(50), nullable=False) action_detail = Column(Text) result = Column(String(20), default='unknown') # success/failed/skipped result_output = Column(Text) duration_ms = Column(Float) created_at = Column(DateTime, default=datetime.now) # Relationships incident = relationship("Incident", backref="heal_logs") playbook = relationship("Playbook", backref="heal_logs") # Seed playbooks for common issues SEED_PLAYBOOKS = [ { 'name': 'Database Connection Recovery', 'error_type': 'database', 'match_pattern': '["connection", "timeout", "unreachable"]', 'action_type': 'WAIT_RETRY', 'action_params': '{"wait_minutes": 5}', 'max_retries': 3, 'cooldown_min': 15 }, { 'name': 'Disk Space Alert', 'error_type': 'disk_space', 'match_pattern': '["disk", "space", "full", "no space"]', 'action_type': 'ALERT_ONLY', 'action_params': '{"message": "Disk space critical - manual intervention required"}', 'max_retries': 1, 'cooldown_min': 60 } ]