# AI history and template models from sqlalchemy import BigInteger, Column, DateTime, Integer, Numeric, String, Text, Boolean, Float from sqlalchemy.dialects import postgresql from sqlalchemy.ext.compiler import compiles from sqlalchemy.types import JSON, UserDefinedType from database.models import Base from datetime import datetime class Vector(UserDefinedType): """pgvector column with a SQLite-safe fallback for local metadata tests.""" cache_ok = True def __init__(self, dimensions): self.dimensions = dimensions def get_col_spec(self, **kw): return f"VECTOR({self.dimensions})" @compiles(Vector, "sqlite") def _compile_vector_sqlite(type_, compiler, **kw): return "TEXT" def _jsonb_type(): return JSON().with_variant(postgresql.JSONB, "postgresql") def _bigint_array_type(): return Text().with_variant(postgresql.ARRAY(BigInteger), "postgresql") class AIGenerationHistory(Base): """ AI generation history tracking """ __tablename__ = 'ai_generation_history' id = Column(Integer, primary_key=True, autoincrement=True) generation_type = Column(String(50), nullable=False) # product_desc, marketing_copy, etc. product_name = Column(String(200)) input_keywords = Column(Text) # JSON string input_trend_topic = Column(String(500)) output_content = Column(Text) generation_duration = Column(Float) # seconds is_favorite = Column(Boolean, default=False) is_used = Column(Boolean, default=False) created_by = Column(String(100)) created_at = Column(DateTime, default=datetime.now) def to_dict(self): return { 'id': self.id, 'generation_type': self.generation_type, 'product_name': self.product_name, 'input_keywords': self.input_keywords, 'input_trend_topic': self.input_trend_topic, 'output_content': self.output_content, 'generation_duration': self.generation_duration, 'is_favorite': self.is_favorite, 'is_used': self.is_used, 'created_by': self.created_by, 'created_at': self.created_at.isoformat() if self.created_at else None } class AIPromptTemplate(Base): """ AI prompt templates """ __tablename__ = 'ai_prompt_templates' id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(200), nullable=False) description = Column(Text) template_type = Column(String(50), nullable=False) # product_desc, marketing_copy, etc. prompt_content = Column(Text, nullable=False) variables = Column(Text) # JSON string of variable definitions is_active = Column(Boolean, default=True) is_system = Column(Boolean, default=False) created_by = Column(String(100)) created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) def to_dict(self): return { 'id': self.id, 'name': self.name, 'description': self.description, 'template_type': self.template_type, 'prompt_content': self.prompt_content, 'variables': self.variables, 'is_active': self.is_active, 'is_system': self.is_system, 'created_by': self.created_by, 'created_at': self.created_at.isoformat() if self.created_at else None, 'updated_at': self.updated_at.isoformat() if self.updated_at else None } class AIUsageTracking(Base): """ AI usage tracking for analytics and billing """ __tablename__ = 'ai_usage_tracking' id = Column(Integer, primary_key=True, autoincrement=True) user_id = Column(String(100)) session_id = Column(String(100)) service_type = Column(String(50), nullable=False) # openai, ollama, etc. model_name = Column(String(100)) prompt_tokens = Column(Integer, default=0) completion_tokens = Column(Integer, default=0) total_tokens = Column(Integer, default=0) cost_usd = Column(Float, default=0.0) request_type = Column(String(50)) # generation, analysis, etc. status = Column(String(20), default='success') # success, failed error_message = Column(Text) duration_ms = Column(Float) created_at = Column(DateTime, default=datetime.now) def to_dict(self): return { 'id': self.id, 'user_id': self.user_id, 'session_id': self.session_id, 'service_type': self.service_type, 'model_name': self.model_name, 'prompt_tokens': self.prompt_tokens, 'completion_tokens': self.completion_tokens, 'total_tokens': self.total_tokens, 'cost_usd': self.cost_usd, 'request_type': self.request_type, 'status': self.status, 'error_message': self.error_message, 'duration_ms': self.duration_ms, 'created_at': self.created_at.isoformat() if self.created_at else None } class AIInsight(Base): """ AI 洞察知識庫(ai_insights 表)— KM 沉澱核心,支援 RAG 向量化。 對應 openclaw_learning_service / scheduler 各任務寫入。 """ __tablename__ = 'ai_insights' id = Column(Integer, primary_key=True, autoincrement=True) insight_type = Column(String(50), nullable=False) # backup_status / human_review / auto_heal_playbook 等 period = Column(String(50)) # 分析週期,e.g. "2026-04-20" product_sku = Column(String(50)) content = Column(Text, nullable=False) metadata_json = Column(Text) # JSON extra payload avg_quality = Column(Float, default=0.5) status = Column(String(20), default='approved') # approved / pending / rejected / archived decay_exempt = Column(Boolean, default=False) ai_model = Column(String(50)) feedback_up = Column(Integer, default=0) feedback_down = Column(Integer, default=0) confidence = Column(Float, default=0.5) created_by = Column(String(50), default='system') created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) # embedding 欄位為 pgvector 型別,透過 raw SQL 寫入,此處不聲明以避免型別衝突 def to_dict(self): return { 'id': self.id, 'insight_type': self.insight_type, 'period': self.period, 'product_sku': self.product_sku, 'content': self.content, 'avg_quality': self.avg_quality, 'status': self.status, 'ai_model': self.ai_model, 'feedback_up': self.feedback_up, 'feedback_down': self.feedback_down, 'confidence': self.confidence, 'created_by': self.created_by, 'created_at': self.created_at.isoformat() if self.created_at else None, } class AICall(Base): """ai_calls unified LLM call telemetry table (migration 024).""" __tablename__ = 'ai_calls' __table_args__ = {'extend_existing': True} id = Column(BigInteger, primary_key=True, autoincrement=True) called_at = Column(DateTime(timezone=True), default=datetime.now, nullable=False) caller = Column(String(64), nullable=False) provider = Column(String(32), nullable=False) model = Column(String(128), nullable=False) input_tokens = Column(Integer, default=0, nullable=False) output_tokens = Column(Integer, default=0, nullable=False) duration_ms = Column(Integer) status = Column(String(16), nullable=False) fallback_to = Column(String(64)) cost_usd = Column(Numeric(10, 6), default=0, nullable=False) cache_hit = Column(Boolean, default=False, nullable=False) rag_hit = Column(Boolean, default=False, nullable=False) request_id = Column(String(64)) error = Column(Text) meta = Column(_jsonb_type()) class MCPCall(Base): """mcp_calls MCP server call telemetry table (migration 025).""" __tablename__ = 'mcp_calls' __table_args__ = {'extend_existing': True} id = Column(BigInteger, primary_key=True, autoincrement=True) called_at = Column(DateTime(timezone=True), default=datetime.now, nullable=False) caller = Column(String(64), nullable=False) server = Column(String(64), nullable=False) tool = Column(String(128), nullable=False) input_args = Column(_jsonb_type()) output_size = Column(Integer) duration_ms = Column(Integer) status = Column(String(16), nullable=False) error = Column(Text) cost_usd = Column(Numeric(10, 6), default=0, nullable=False) cache_hit = Column(Boolean, default=False, nullable=False) request_id = Column(String(64)) insight_id = Column(BigInteger) class AICallBudget(Base): """ai_call_budgets budget guardrail table (migration 025).""" __tablename__ = 'ai_call_budgets' __table_args__ = {'extend_existing': True} id = Column(Integer, primary_key=True, autoincrement=True) period = Column(String(16), nullable=False) provider = Column(String(32)) budget_usd = Column(Numeric(10, 2), nullable=False) alert_pct = Column(Integer, default=80, nullable=False) updated_at = Column(DateTime(timezone=True), default=datetime.now) class RAGQueryLog(Base): """rag_query_log RAG recall telemetry table (migration 027).""" __tablename__ = 'rag_query_log' __table_args__ = {'extend_existing': True} id = Column(BigInteger, primary_key=True, autoincrement=True) queried_at = Column(DateTime(timezone=True), default=datetime.now, nullable=False) caller = Column(String(64), nullable=False) query_text = Column(Text, nullable=False) query_embedding = Column(Vector(1024)) embedding_signature = Column(String(64)) top_k = Column(Integer, default=5, nullable=False) threshold = Column(Numeric(4, 3), default=0.85, nullable=False) hit_count = Column(Integer, default=0, nullable=False) used_results = Column(_bigint_array_type()) saved_call = Column(Boolean, default=False, nullable=False) feedback_score = Column(Integer) request_id = Column(String(64)) class LearningEpisode(Base): """learning_episodes PromotionGate staging table (migration 028).""" __tablename__ = 'learning_episodes' __table_args__ = {'extend_existing': True} id = Column(BigInteger, primary_key=True, autoincrement=True) created_at = Column(DateTime(timezone=True), default=datetime.now, nullable=False) episode_type = Column(String(32), nullable=False) source_table = Column(String(32)) source_id = Column(BigInteger) distilled_text = Column(Text, nullable=False) embedding = Column(Vector(1024)) embedding_signature = Column(String(64)) quality_score = Column(Numeric(4, 3), default=0.0, nullable=False) weight = Column(Numeric(4, 3), default=0.5, nullable=False) promotion_status = Column(String(32), default='pending', nullable=False) insight_id = Column(BigInteger) rejected_reason = Column(Text) human_approver = Column(String(64)) reviewed_at = Column(DateTime(timezone=True)) class HostHealthProbe(Base): """host_health_probes Ollama failover health history table (migration 029).""" __tablename__ = 'host_health_probes' __table_args__ = {'extend_existing': True} id = Column(BigInteger, primary_key=True, autoincrement=True) probed_at = Column(DateTime(timezone=True), default=datetime.now, nullable=False) host_label = Column(String(64), nullable=False) host_url = Column(String(256), nullable=False) healthy = Column(Boolean, nullable=False) unhealthy_mark = Column(Boolean, default=False, nullable=False) models_count = Column(Integer, default=0) response_ms = Column(Integer) error_msg = Column(Text) class PPTAuditResult(Base): """ppt_audit_results PPT vision audit history table (migration 030).""" __tablename__ = 'ppt_audit_results' __table_args__ = {'extend_existing': True} id = Column(BigInteger, primary_key=True, autoincrement=True) audited_at = Column(DateTime(timezone=True), default=datetime.now, nullable=False) pptx_filename = Column(String(256), nullable=False) pptx_size_kb = Column(Integer) pptx_mtime = Column(DateTime(timezone=True)) vision_enabled = Column(Boolean, nullable=False) audit_status = Column(String(32), nullable=False) issues_count = Column(Integer, default=0) issues_found = Column(_jsonb_type()) confidence = Column(Numeric(4, 3)) duration_ms = Column(Integer) error_msg = Column(Text) reviewer_notes = Column(Text) __all__ = [ "AIGenerationHistory", "AIPromptTemplate", "AIUsageTracking", "AIInsight", "AICall", "MCPCall", "AICallBudget", "RAGQueryLog", "LearningEpisode", "HostHealthProbe", "PPTAuditResult", ]