329 lines
13 KiB
Python
329 lines
13 KiB
Python
# 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",
|
||
]
|