All checks were successful
CD Pipeline / deploy (push) Successful in 1m14s
- 新增 services/mcp_collector_service.py:Gemini Search Grounding 外部情報收集 - 重寫 services/openclaw_strategist_service.py:真實 Gemini 2.5 Flash 分析,DB 持久化 - scheduler.py:修復 generate_meta_analysis_report ImportError,串接 Meta-Analysis - elephant_alpha_autonomous_engine.py:新增 weekly_insight 觸發器路由 OpenClaw Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
226 lines
8.9 KiB
Python
226 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
services/mcp_collector_service.py
|
||
MCP 外部情報收集層
|
||
|
||
透過 Gemini Google Search Grounding 收集外部市場情報,供 OpenClaw 戰略分析使用:
|
||
- 台灣電商市場趨勢
|
||
- 節日 / 促銷行事曆
|
||
- 季節性消費洞察
|
||
- 競品動態(蝦皮/PChome/momo/Yahoo)
|
||
- 消費者情緒與熱銷品類
|
||
|
||
結果快取至 ai_insights(type='mcp_cache'),24h TTL 避免重複呼叫。
|
||
"""
|
||
|
||
import json
|
||
import logging
|
||
import os
|
||
import time
|
||
from datetime import datetime, timedelta
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from database.manager import get_session
|
||
from sqlalchemy import text
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
|
||
MCP_CACHE_TTL_HOURS = int(os.getenv("MCP_CACHE_TTL_HOURS", "24"))
|
||
MCP_MODEL = os.getenv("MCP_GEMINI_MODEL", "gemini-2.5-flash-preview-05-20")
|
||
|
||
# ── 查詢主題定義 ────────────────────────────────────────────────────────────
|
||
_SEARCH_TOPICS = {
|
||
"market_trends": (
|
||
"台灣電商 momo購物網 2025年熱銷商品趨勢 消費者行為 美妝保養 家電 生活用品"
|
||
),
|
||
"holiday_calendar": (
|
||
"2025年台灣重要節日促銷行事曆 母親節 618購物節 雙11 雙12 中秋 跨年 電商大促"
|
||
),
|
||
"seasonal_insights": (
|
||
"台灣電商季節性銷售趨勢 換季商品 夏季防曬 冬季保暖 Q3 Q4 消費高峰"
|
||
),
|
||
"competitor_intel": (
|
||
"momo購物網 PChome 蝦皮 Yahoo購物 2025年競爭策略 促銷活動 物流比較"
|
||
),
|
||
"consumer_sentiment": (
|
||
"台灣消費者 2025 購物偏好 低價高CP 品牌忠誠度 直播購物 社群電商 KOL影響"
|
||
),
|
||
"pricing_strategy": (
|
||
"台灣電商定價策略 動態定價 競品比價 心理定價 促銷折扣最佳時機"
|
||
),
|
||
}
|
||
|
||
|
||
class MCPCollectorService:
|
||
"""
|
||
外部情報收集服務(MCP 節點)
|
||
使用 Gemini Search Grounding 抓取即時市場資訊
|
||
"""
|
||
|
||
def __init__(self):
|
||
self._initialized = False
|
||
self._genai = None
|
||
|
||
def _ensure_init(self) -> bool:
|
||
if self._initialized:
|
||
return True
|
||
if not GEMINI_API_KEY:
|
||
logger.warning("[MCP] GEMINI_API_KEY 未設定,跳過外部情報收集")
|
||
return False
|
||
try:
|
||
import google.generativeai as genai
|
||
genai.configure(api_key=GEMINI_API_KEY)
|
||
self._genai = genai
|
||
self._initialized = True
|
||
return True
|
||
except ImportError:
|
||
logger.error("[MCP] google-generativeai 未安裝")
|
||
return False
|
||
except Exception as e:
|
||
logger.error("[MCP] Gemini 初始化失敗: %s", e)
|
||
return False
|
||
|
||
# ── 快取讀寫 ────────────────────────────────────────────────────────────
|
||
|
||
def _read_cache(self, topic: str) -> Optional[str]:
|
||
session = get_session()
|
||
try:
|
||
row = session.execute(
|
||
text(f"""
|
||
SELECT content FROM ai_insights
|
||
WHERE insight_type = 'mcp_cache'
|
||
AND created_by = 'mcp_collector'
|
||
AND metadata_json::jsonb ->> 'topic' = :topic
|
||
AND created_at >= NOW() - INTERVAL '{MCP_CACHE_TTL_HOURS} hours'
|
||
ORDER BY created_at DESC LIMIT 1
|
||
"""),
|
||
{"topic": topic},
|
||
).fetchone()
|
||
if row:
|
||
logger.debug("[MCP] 快取命中: %s", topic)
|
||
return row[0]
|
||
return None
|
||
except Exception:
|
||
return None
|
||
finally:
|
||
session.close()
|
||
|
||
def _write_cache(self, topic: str, content: str) -> None:
|
||
session = get_session()
|
||
try:
|
||
session.execute(text("""
|
||
INSERT INTO ai_insights
|
||
(insight_type, content, confidence, created_by, status, metadata_json)
|
||
VALUES ('mcp_cache', :content, 0.9, 'mcp_collector', 'active', :meta)
|
||
"""), {
|
||
"content": content[:4000],
|
||
"meta": json.dumps({"topic": topic, "model": MCP_MODEL, "cached_at": datetime.now().isoformat()})
|
||
})
|
||
session.commit()
|
||
except Exception as e:
|
||
logger.warning("[MCP] 快取寫入失敗: %s", e)
|
||
session.rollback()
|
||
finally:
|
||
session.close()
|
||
|
||
# ── 單主題搜尋 ──────────────────────────────────────────────────────────
|
||
|
||
def _search_topic(self, topic: str, query: str) -> str:
|
||
if not self._ensure_init():
|
||
return ""
|
||
|
||
cached = self._read_cache(topic)
|
||
if cached:
|
||
return cached
|
||
|
||
try:
|
||
model = self._genai.GenerativeModel(
|
||
model_name=MCP_MODEL,
|
||
tools=["google_search_retrieval"],
|
||
)
|
||
response = model.generate_content(
|
||
f"請用繁體中文整理以下主題的最新資訊,提供具體數據與洞察,500字以內:\n{query}"
|
||
)
|
||
content = response.text or ""
|
||
if content:
|
||
self._write_cache(topic, content)
|
||
return content
|
||
except Exception as e:
|
||
logger.warning("[MCP] 搜尋失敗 topic=%s: %s", topic, e)
|
||
return ""
|
||
|
||
# ── 公開介面 ────────────────────────────────────────────────────────────
|
||
|
||
def collect_all(self) -> Dict[str, str]:
|
||
"""
|
||
收集所有外部情報主題,回傳 {topic: content} 字典。
|
||
各主題獨立失敗不影響整體。
|
||
"""
|
||
results = {}
|
||
for topic, query in _SEARCH_TOPICS.items():
|
||
try:
|
||
results[topic] = self._search_topic(topic, query)
|
||
time.sleep(0.5) # 避免 Gemini rate limit
|
||
except Exception as e:
|
||
logger.error("[MCP] topic=%s 收集失敗: %s", topic, e)
|
||
results[topic] = ""
|
||
logger.info("[MCP] 收集完成,有效主題=%d/%d", sum(1 for v in results.values() if v), len(results))
|
||
return results
|
||
|
||
def collect_topic(self, topic: str) -> str:
|
||
"""收集單一主題"""
|
||
query = _SEARCH_TOPICS.get(topic, topic)
|
||
return self._search_topic(topic, query)
|
||
|
||
def get_holiday_context(self) -> str:
|
||
"""取得節日行事曆(供 Prompt 注入)"""
|
||
now = datetime.now()
|
||
month = now.month
|
||
|
||
# 靜態台灣電商節日知識庫(無需 API 呼叫)
|
||
static_calendar = {
|
||
1: "元旦促銷、農曆新年備貨期(1/20前後開始)",
|
||
2: "農曆新年(年貨、禮盒熱賣)、情人節(2/14,保養/禮品衝量)",
|
||
3: "婦女節(3/8)、春季換季保養、開學季",
|
||
4: "清明連假、春季大促、換季服飾高峰",
|
||
5: "母親節(5/2週前後,美妝/保健/家電最高峰)、520情人節",
|
||
6: "618購物節(最大中年促銷,全平台衝量)、父親節備檔",
|
||
7: "父親節(7/4週前後)、暑假家電/3C/旅遊用品高峰",
|
||
8: "七夕情人節(8/10前後)、暑假尾聲出清",
|
||
9: "中秋節(禮盒/食品衝量)、開學季3C/文具",
|
||
10: "雙10國慶、品牌週年慶(百貨、電商 10月旺季)",
|
||
11: "雙11光棍節(全年最大促銷)、品牌大促備貨",
|
||
12: "雙12年終慶、聖誕節(12/25)、跨年(元旦備貨)",
|
||
}
|
||
base = static_calendar.get(month, "")
|
||
|
||
# 加入下個月預告
|
||
next_month = (month % 12) + 1
|
||
next_base = static_calendar.get(next_month, "")
|
||
|
||
return (
|
||
f"當前月份:{now.strftime('%Y年%m月')}\n"
|
||
f"本月電商重點:{base}\n"
|
||
f"下月預告:{next_base}"
|
||
)
|
||
|
||
def get_seasonal_context(self) -> str:
|
||
"""季節性消費情境"""
|
||
month = datetime.now().month
|
||
seasons = {
|
||
(3, 4, 5): "春季:換季保養、外出服飾、春遊裝備",
|
||
(6, 7, 8): "夏季:防曬/美白、涼感寢具、戶外運動、冷氣清潔",
|
||
(9, 10, 11): "秋季:保濕修護、秋冬服飾、保健養生、熱飲週邊",
|
||
(12, 1, 2): "冬季:保暖寢具、暖身家電、年節禮品、養生補品",
|
||
}
|
||
for months, desc in seasons.items():
|
||
if month in months:
|
||
return desc
|
||
return ""
|
||
|
||
|
||
# 模組單例
|
||
mcp_collector = MCPCollectorService()
|