#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ AI 生成歷史記錄資料庫模型 儲存 Ollama/Gemini LLM 生成的文案和推薦結果 支援多 AI 提供者和費用追蹤 """ from sqlalchemy import Column, Integer, String, Float, DateTime, Date, Text, Boolean, ForeignKey, Index from sqlalchemy.orm import relationship from datetime import datetime, date from .models import Base class AIGenerationHistory(Base): """AI 生成歷史記錄表""" __tablename__ = 'ai_generation_history' id = Column(Integer, primary_key=True) # 生成類型:copy (文案), recommend (推薦), weather_analysis (天氣分析) generation_type = Column(String(50), nullable=False, index=True) # 商品相關 product_name = Column(String(255), index=True) # 輸入參數 input_keywords = Column(Text) # JSON 格式的關鍵字列表 input_style = Column(String(50)) # 文案風格 input_trend_topic = Column(Text) # 趨勢話題(用於推薦) # 生成結果 output_content = Column(Text, nullable=False) # 生成的內容 # 模型資訊 model_name = Column(String(100)) generation_duration = Column(Float) # 生成耗時(秒) # AI 提供者資訊 (新增 - 支援 Ollama/Gemini 切換) ai_provider = Column(String(20), default='ollama') # 'ollama' 或 'gemini' input_tokens = Column(Integer, default=0) # 輸入 token 數量 (用於 Gemini 費用計算) output_tokens = Column(Integer, default=0) # 輸出 token 數量 # 評價與狀態 rating = Column(Integer) # 用戶評分 1-5 is_favorite = Column(Boolean, default=False) # 是否收藏 is_used = Column(Boolean, default=False) # 是否已使用 notes = Column(Text) # 用戶備註 # 用戶追蹤 created_by = Column(Integer, ForeignKey('users.id')) created_at = Column(DateTime, default=datetime.now, index=True) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) # 建立索引以優化查詢 __table_args__ = ( Index('idx_ai_history_type_created', 'generation_type', 'created_at'), Index('idx_ai_history_product', 'product_name'), Index('idx_ai_history_favorite', 'is_favorite', 'created_at'), ) def to_dict(self): """轉換為字典格式""" import json return { 'id': self.id, 'generation_type': self.generation_type, 'product_name': self.product_name, 'input_keywords': json.loads(self.input_keywords) if self.input_keywords else [], 'input_style': self.input_style, 'input_trend_topic': self.input_trend_topic, 'output_content': self.output_content, 'model_name': self.model_name, 'generation_duration': self.generation_duration, 'ai_provider': self.ai_provider, 'input_tokens': self.input_tokens, 'output_tokens': self.output_tokens, 'rating': self.rating, 'is_favorite': self.is_favorite, 'is_used': self.is_used, 'notes': self.notes, '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 使用量追蹤表 追蹤 Gemini API 費用和所有 AI 使用統計 """ __tablename__ = 'ai_usage_tracking' id = Column(Integer, primary_key=True) # AI 提供者: 'ollama', 'gemini' provider = Column(String(20), nullable=False, index=True) # 模型名稱: 'gemma3:4b', 'gemini-1.5-flash', 'gemini-2.5-pro' model_name = Column(String(100), nullable=False) # 使用類型: 'copy', 'web_search', 'product_insights', 'trend_keywords' usage_type = Column(String(50), nullable=False) # Token 用量 input_tokens = Column(Integer, default=0) output_tokens = Column(Integer, default=0) total_tokens = Column(Integer, default=0) # 費用計算 (USD) - 主要用於 Gemini input_cost = Column(Float, default=0.0) output_cost = Column(Float, default=0.0) total_cost = Column(Float, default=0.0) # 響應時間 (秒) duration = Column(Float) # 請求資訊 request_date = Column(Date, nullable=False, index=True) created_at = Column(DateTime, default=datetime.now) created_by = Column(Integer, ForeignKey('users.id')) # 關聯到歷史記錄 (可選) history_id = Column(Integer, ForeignKey('ai_generation_history.id')) __table_args__ = ( Index('idx_usage_provider_date', 'provider', 'request_date'), Index('idx_usage_model_date', 'model_name', 'request_date'), ) def to_dict(self): """轉換為字典格式""" return { 'id': self.id, 'provider': self.provider, 'model_name': self.model_name, 'usage_type': self.usage_type, 'input_tokens': self.input_tokens, 'output_tokens': self.output_tokens, 'total_tokens': self.total_tokens, 'input_cost': self.input_cost, 'output_cost': self.output_cost, 'total_cost': self.total_cost, 'duration': self.duration, 'request_date': self.request_date.isoformat() if self.request_date else None, 'created_at': self.created_at.isoformat() if self.created_at else None, } class AIPromptTemplate(Base): """AI 提示模板表 - 儲存常用的提示詞模板""" __tablename__ = 'ai_prompt_templates' id = Column(Integer, primary_key=True) name = Column(String(100), nullable=False, unique=True) # 模板名稱 description = Column(String(255)) # 模板描述 template_type = Column(String(50), nullable=False, index=True) # copy, recommend, analysis system_prompt = Column(Text) # 系統提示詞 user_prompt_template = Column(Text, nullable=False) # 用戶提示詞模板 # 預設參數 default_temperature = Column(Float, default=0.7) default_style = Column(String(50)) is_active = Column(Boolean, default=True) is_system = Column(Boolean, default=False) # 是否為系統內建 created_by = Column(Integer, ForeignKey('users.id')) 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, 'system_prompt': self.system_prompt, 'user_prompt_template': self.user_prompt_template, 'default_temperature': self.default_temperature, 'default_style': self.default_style, 'is_active': self.is_active, 'is_system': self.is_system, 'created_at': self.created_at.isoformat() if self.created_at else None, } # 預設的提示模板 DEFAULT_PROMPT_TEMPLATES = [ { 'name': '吸睛電商文案', 'description': '適合吸引眼球的促銷文案', 'template_type': 'copy', 'system_prompt': '''你是一位專業的電商銷售文案寫手,專門為台灣電商平台撰寫商品文案。 你的文案特點: - 使用繁體中文 - 簡潔有力,通常在 100 字以內 - 善用表情符號增加吸引力 - 強調商品賣點和消費者利益 - 適時使用行動呼籲 (CTA)''', 'user_prompt_template': '''請為以下商品撰寫銷售文案: 商品名稱:{product_name} 文案風格:使用吸引眼球的標題和表情符號 {trend_context} 請生成一段吸引人的銷售文案(100字以內):''', 'default_temperature': 0.8, 'default_style': '吸睛', 'is_system': True, }, { 'name': '專業產品介紹', 'description': '適合強調功效和成分的專業文案', 'template_type': 'copy', 'system_prompt': '''你是一位專業的產品行銷專家,擅長撰寫專業且有說服力的產品介紹。 你的文案特點: - 使用繁體中文 - 強調產品的專業性和科學依據 - 使用精確的數據和專業術語 - 建立品牌信任感''', 'user_prompt_template': '''請為以下商品撰寫專業介紹: 商品名稱:{product_name} 文案風格:使用專業術語,強調成分和功效 {trend_context} 請生成一段專業的產品介紹(100字以內):''', 'default_temperature': 0.5, 'default_style': '專業', 'is_system': True, }, { 'name': '限時促銷文案', 'description': '創造緊迫感的促銷文案', 'template_type': 'copy', 'system_prompt': '''你是一位擅長製造緊迫感的行銷文案專家。 你的文案特點: - 使用繁體中文 - 善用限時、限量等字眼 - 創造錯過可惜的感覺 - 強調立即行動的好處''', 'user_prompt_template': '''請為以下商品撰寫限時促銷文案: 商品名稱:{product_name} 文案風格:使用限時優惠的語氣,創造緊迫感 {trend_context} 請生成一段有緊迫感的促銷文案(100字以內):''', 'default_temperature': 0.7, 'default_style': '急迫', 'is_system': True, }, ] class AIInsight(Base): """ AI 洞察與知識庫表 (符合 ADR-007 雙寫規範) Step 2 加入,供 OpenClaw 保存歷史 PPT、分析等輸出。 (embedding 欄位將在 Step 3 透過 SQL ALTER 增加,不宣告於 SQLAlchemy,避免 SQLite 相容性錯誤) """ __tablename__ = 'ai_insights' id = Column(Integer, primary_key=True, autoincrement=True) insight_type = Column(String(50), nullable=False, index=True) # ppt, competitor_analysis, weekly_meta period = Column(String(50), index=True) # 2026-04-16, 2026-W15 product_sku = Column(String(50), index=True) # 如果針對單一商品 content = Column(Text, nullable=False) # 具體輸出內容 metadata_json = Column(Text) # 附加元數據 (JSON 字串) created_at = Column(DateTime, default=datetime.now, index=True) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) def to_dict(self): import json return { 'id': self.id, 'insight_type': self.insight_type, 'period': self.period, 'product_sku': self.product_sku, 'content': self.content, 'metadata': json.loads(self.metadata_json) if self.metadata_json else {}, 'created_at': self.created_at.isoformat() if self.created_at else None, } def init_ai_tables(session): """ 初始化 AI 相關表和預設資料 Args: session: SQLAlchemy session Returns: tuple: (success: bool, message: str) """ try: # 檢查是否已有預設模板 existing_count = session.query(AIPromptTemplate).filter_by(is_system=True).count() if existing_count == 0: # 新增預設模板 for template_data in DEFAULT_PROMPT_TEMPLATES: template = AIPromptTemplate(**template_data) session.add(template) session.commit() return True, f"AI 模板初始化完成,新增 {len(DEFAULT_PROMPT_TEMPLATES)} 個預設模板" else: return True, f"AI 模板已存在 ({existing_count} 個系統模板)" except Exception as e: session.rollback() return False, f"AI 模板初始化失敗: {e}"