Files
ewoooc/docs/AI_INTELLIGENCE_MODULE_SOT.md
OoO 108b05cc8b
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s
收緊 PChome 單位價可比邊界
2026-05-19 23:49:23 +08:00

608 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MOMO PRO — AI 競價情報模組 Single Source of Truth
> **最後更新**: 2026-05-19 (台北時間)
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地LLM 路由紅線升級為 Ollama-first 三主機級聯Gemini 僅備援 / 鎖定場景
> **適用版本**: V10.292
---
## 零、LLM 路由紅線2026-05-12
- 所有 AI Agent、LLM 推理與 embedding 預設必須走 Ollama 三主機級聯GCP-A `34.143.170.20:11434` → GCP-B `34.21.145.224:11434` → 111 `192.168.0.111:11434`
- `services/ollama_service.resolve_ollama_host()` 是主機解析契約;`OLLAMA_HOST``HERMES_URL``EMBEDDING_HOST``OLLAMA_API_BASE` 只接受 GCP-A / GCP-B / 111 或 110 的核准轉發端口。
- Gemini 只能作為 Ollama 主路徑失敗後的備援,或 ADR-028 明確鎖定的 MCP Grounding、PPT/vision、週/月報、Code Review、EA HITL、複雜 SKU 升級等低頻場景。
- 188 `192.168.0.188` 僅是 App / DB / scheduler / Telegram bot 容器宿主與 AutoHeal target不可作為 Ollama 節點。
- 通用 AI 文案、關鍵字、商品洞察與 Telegram Q&A 第一響應不得 Gemini-first。
- Hermes intent / analyst 路徑不得手刻 `/api/generate` 或只 resolve 單次 host必須走 `OllamaService`,讓同一請求可依序 retry GCP-A → GCP-B → 111。
- NemoTron qwen3 dispatch 的 `/api/chat` tool-calling 路徑也必須同一請求最多嘗試三台 Ollama第一台失敗要 `mark_unhealthy()` 後再試下一台,最後才 fallback NIM。
- PPT vision、PPT 文案 final fallback、MCP 離線 final fallback 等特殊 Ollama 路徑也不得只打單一 host如需 `/api/generate`,一律透過 `OllamaService.generate()`
- Code Review pipeline 也必須 Ollama-firstHermes scan 與 OpenClaw assessment 都走 `OllamaService` 三主機 retryGemini telemetry 只能以 `code_review_openclaw_gemini` 出現,表示 Ollama/可選 Claude 備援都失敗後才啟用。
- Code Review Hermes scan 預設不呼叫 LLM改用 deterministic fast static scan避免部署後先卡三段 Ollama timeout需要 LLM 掃描時才以 `CODE_REVIEW_HERMES_LLM_SCAN_ENABLED=true` 啟用本地矩陣。
- Code Review Hermes LLM scan 啟用時才使用本地模型矩陣GCP-A `qwen2.5-coder:7b`、GCP-B `gemma3:4b`、111 `hermes3:latest`;不啟用 Gemini 備援,三段本地掃描失敗時只回空 findings 並交由 OpenClaw 本地矩陣續跑。
- Code Review OpenClaw assessment 保持主機順序 GCP-A → GCP-B → 111但可使用主機適配本地模型GCP-A `qwen2.5-coder:7b`、GCP-B `gemma3:4b`、111 `hermes3:latest`;三段本地 Ollama 全失敗後才允許雲端備援。
- OpenClaw Telegram Q&A 主路徑也不得綁單一 host`_call_qwen3_qa()` 必須透過 `OllamaService` 跑 GCP-A → GCP-B → 111並把實際落點寫入 `ai_calls.provider`
- OpenClaw Telegram 圖片商品辨識也必須 Ollama-first`_identify_product_name_with_ollama_vision()` 透過 `OllamaService` 嘗試 GCP-A → GCP-B → 111Gemini 只允許以 `openclaw_bot_image_gemini` caller 作為失敗後備援。
## 一、四 AI Agent 路由架構
```
SQL漏斗(~300筆)
[Hermes 3 8B] — 分析師 (Ollama 三主機級聯, 零成本)
模型: hermes3:latest @ GCP-A → GCP-B → 111
任務: 競價威脅分類 → TOP 20 HIGH/MED/LOW
[NemoTron / qwen3] — 派發器
主路徑: qwen3:14b @ Ollama 三主機級聯
備援: NVIDIA NIM meta/llama-3.1-8b-instruct
任務: Tool Calling → Telegram 告警 / DB 寫入
[OpenClaw] — 策略師 (Ollama-firstGemini 僅備援 / 鎖定場景)
任務: 週策略報告、洞察報告、L3 HITL 建議
[ElephantAlpha] — 編排者 (L3 Orchestrator)
任務: 跨 Agent orchestration、HITL、AutoHeal bridge、受控 log scan
```
### 1.1 PChome 挑品 Agent2026-05-01
`services/ai_product_pick_agent.py` 新增 PChome 銷售用挑品 Agent
- 只讀真實資料表:`products``price_records``competitor_prices``competitor_price_history`,若 `daily_sales_snapshot` 可用則納入近 7 天銷售額、數量、毛利或成本推算毛利率。
- 將 PChome 比 MOMO 有價格優勢、比對信心足夠、且有歷史快照或銷售動能的品項寫入 `ai_price_recommendations`。信心度不以固定倍率灌高,而是由商機分數與證據完整度共同決定,證據包含 PChome match score、歷史快照、銷售/毛利、PChome 商品 ID/名稱、抓取時間與促銷/評價/庫存標籤。每次重算只保留最新 50 品為 `status='pending'`,未進榜舊品標為 `superseded`,避免統計與清單超量。
- 寫入策略使用 `strategy='product_pick'`,保留在既有 AI 決策表,不新增假頁面或暫存 JSON。
- 後台入口:`POST /api/ai/product-picks/generate``/ai_intelligence` 可手動產生清單。
- 配對來源仍以 PChome crawler 真實搜尋結果為準;無競品資料時不生成挑品。
- 比對覆蓋率補強入口:`POST /api/ai/pchome-match/backfill`,優先補抓仍無有效 PChome 配對的高價 ACTIVE 商品,完成後自動重算 AI 挑品清單。
- 排程閉環:`run_pchome_match_backfill_task` 每日 10:30 執行,補抓 PChome 待比對商品、寫入歷史價格,再重算 `strategy='product_pick'` 清單。
- PChome / MOMO 競價摘要出口 `services/competitor_intel_repository.py` 使用 30 分鐘共享快取(`COMPETITOR_INTEL_CACHE_TTL_SECONDS` 可調),避免 `/growth_analysis``/daily_sales`、PPT/AI 報表每次請求重跑昂貴覆蓋率與價差趨勢查詢;`run_competitor_price_feeder_task` 與 PChome backfill 完成後會主動清除快取。快取只包摘要輸出,不改 matcher 的高信心門檻與 identity_v2 準確性規則。
- 商品看板第一屏:`/` 的 V2 看板直接以 `products``price_records``competitor_prices``ai_price_recommendations` 顯示比對覆蓋率、PChome 優勢、MOMO 威脅、AI 挑品與待比對優先清單;`filter=ai_picks` 可查看 50 品 AI 挑品列表,並在列表上方顯示平均信心、平均價差、最大價差與估算總價差空間,列表列內顯示 AI 排名與建議理由,且可透過 `/api/export/excel/ai-picks` 匯出 50 品 Excel 操作清單。商品看板深度快取同時寫入 `data/dashboard_full_cache.pkl`,供多個 Gunicorn worker 共用,避免部署後各 worker 重複重建 7,000+ 商品統計造成開頁變慢;所有資料異動與 AI 挑品重算都透過 `clear_dashboard_cache()` 同步清除記憶體與共享快取,手動重算 API 會立即預熱商品看板快取,避免第一位使用者承擔重建成本。
| 角色 | 模型 | 主機 | 成本 | 每日限額 |
|------|------|------|------|---------|
| Hermes 分析師 | hermes3:latest / bge-m3 | GCP-A → GCP-B → 111 Ollama | 零 | 無限 |
| NemoTron 派發器 | qwen3:14bNIM fallback | GCP-A → GCP-B → 111NVIDIA NIM 備援 | Ollama 零NIM 配額內免費 | NIM 80 |
| OpenClaw 策略師 | qwen3:14b / Gemini 鎖定場景 | Ollama-firstGemini 備援 | Ollama 零Gemini 需控管 | — |
| ElephantAlpha 編排者 | ElephantAlpha | 依部署環境 | 受控 | HITL / 任務制 |
---
## 一之一、AI 自動化閉環實況2026-04-29
```text
事件 / 排程失敗 / code review finding
→ EventRouter 分流、去重、降級
→ Hermes L1 摘要或 NemoTron L2 tool calling
→ L2 SAFE_ACTIONS / AutoHeal / OpenClaw memory
→ Telegram 通知,失敗則 file queue成功後 replay
→ ai_insights + embedding_retry_queue
→ OpenClaw / ElephantAlpha 後續策略與 HITL
```
硬性邊界:
- EventRouter 是告警與 L2 safe action 的入口。
- AutoHeal 是自癒副作用入口。
- `momo-db` / `momo-postgres` 不可被 AI 自動 restart / stop / recreate。
- raw `ai_insights` insert 必須接 `enqueue_insight_embedding()` 或可被 backfill。
- ElephantAlpha 只做編排與 bridge不可繞過 ADR-011 / ADR-012 / ADR-013。
- ElephantAlpha / NemoTron 不可直接執行商品價格調整;`execute_price_adjustment``adjust_price` 等動作必須攔截並寫入 `human_review`,等待人工核准。
可觀測性:
- `/metrics` 匯出 `momo_ai_event_router_dispatch_total`
- `/metrics` 匯出 `momo_ai_event_router_latency_ms_count/sum/max`
- `/metrics` 匯出 `momo_ai_event_router_safe_action_total`
- `/metrics` 匯出 `momo_ai_event_router_replay_total`
- `/metrics` 匯出 `momo_ai_autoheal_action_total``momo_ai_autoheal_duration_ms_count/sum/max`
- `/metrics` 在尚無事件時仍輸出 `momo_ai_*` zero-baseline series讓 Prometheus/Grafana 重啟後可立即看到 metric names。
- `/ai_automation_smoke` 提供登入後 smoke dashboard。
- `/api/ai-automation/smoke` 提供 read-only JSON 狀態,不做外部網路呼叫。
- Smoke API 會將最近快檢結果保存到 JSONLdashboard 顯示最近狀態趨勢。
- Smoke history 支援 JSONL 匯出、清理與每日 OK / Warning / Critical 摘要。
- Smoke 每日摘要支援手動 Telegram 推播,並由 `momo-scheduler` 每日 09:10 呼叫 `run_ai_smoke_daily_summary_task()`
- Grafana provisioning 新增 `docker/grafana/provisioning/dashboards/json/ai-automation-overview.json`,觀測 EventRouter dispatch/latency、safe action、Telegram replay 與 AutoHeal action/duration。
- Active monitoring stack 使用 `monitoring/prometheus.yml``momo-app` job scrape `momo-pro-system:80/metrics`Prometheus container 需加入 `momo-network`
- Active Blackbox HTTP targets 必須探測 `/health`188 stack 目前 `https://mo.wooo.work/health``http://momo-pro-system:80/health`110 gateway stack 目前 `https://mo.wooo.work/health`),不可探測 Dashboard 首頁 `/`,避免監控流量觸發重型 DB 查詢。
- `/metrics``realtime_sales_monthly` 只用 raw `SELECT COUNT(*)` 取得總筆數,避免 ORM schema drift 讓 Prometheus scrape 產生 warning。
- `momo-app` 必須 bind mount `./gunicorn.conf.py:/app/gunicorn.conf.py:ro`,讓 CD sync/rebuild 後的 Gunicorn runtime 設定與 repo 保持一致。
- Gunicorn runtime 預設 `worker_class = gthread``GUNICORN_THREADS=4``preload_app = False`;此組合讓 HUP 熱重載可用,也避免 Dashboard 長查詢完全阻塞 `/health`
- CD rebuild 模式必須先 build image 成功,再短暫 stop/rm/recreate 三應用容器,避免 no-cache build 造成長時間 502。
- ElephantAlpha 使用 NVIDIA NIM hosted APIproduction 預設模型為 `nvidia/llama-3.3-nemotron-super-49b-v1.5``ELEPHANT_ALPHA_FALLBACK_MODELS` 需保留至少一個可呼叫備援403/404、408/409/425/429、5xx、timeout 與 connection error 必須嘗試下一個模型。
- ElephantAlpha L3 HITL 只允許發送有實證、可審核、可行動的升級告警;價格類 trigger 無 Hermes 具體威脅時,只記錄 suppressed escalation telemetry 與 cooldown不寫 pending `human_review`,不發 Telegram 空告警。
- ElephantAlpha 價格類 trigger 的 HITL / 決策 prefetch 必須先使用觸發 SQL 與 `competitor_prices` / `price_records` 的 DB 實證生成 SKU、MOMO / PChome 價差與建議 action lines完整 Hermes LLM prefetch 預設關閉(`ELEPHANT_ALPHA_HERMES_LLM_PREFETCH_ENABLED=false`),避免 5s timeout 後落入無實證摘要或雲端備援。若無 DB 實證,只記錄 suppressed telemetry / cooldown不發 Telegram 空告警。
- `resource_optimization` 不再交給 LLM 生成「預期效益 / 已執行」敘事,顯示名稱統一為「資源壓力治理」。此 trigger 必須先由程式量測 `action_plans` backlog、P1/P2 數、pending_review、逾時項目與 CPU load只有 CPU 達門檻、P1/P2 積壓或逾時積壓才發 Telegram「資源壓力告警」。單純 queue 大但 CPU 正常只記錄 telemetry不派發 Hermes/NemoTron、不宣稱 48 小時效益Telegram 段落使用「系統處置紀錄」而非泛稱「已執行」,避免暗示 AI 已完成未經驗證的外部動作。
- `resource_optimization` 會先執行 `ActionPlanHygieneService` 清理過期噪音:只關閉超過 72 小時的 `code_review_fix` / `openclaw_recommendation` 類 advisory action_plans以及 NemoTron `direct_response/reply_simple` 舊聊天回覆計畫;將狀態改為 `auto_disabled``rejected` 並寫入 `metadata_json.hygiene_history`。不刪資料,也不碰 NemoTron human_review / pricing / tool action 類業務行動。
- `momo-scheduler` 每 6 小時固定執行 `run_action_plan_hygiene_task()`,讓過期 advisory action_plans 的關閉不再依賴 `resource_optimization` 告警觸發;排程失敗會經 EventRouter 發送 `action_plan_hygiene_failure`
- `action_plans` 產生端必須防重Code Review 同一檔案已有 active `code_review_fix` 時不重建OpenClaw recommendation 會寫入文字 fingerprint 並跳過同一建議AIOrchestrator 不再把 NemoTron `direct_response/reply_simple` 聊天回覆存成 action plan真正需工具、審核或執行的 NemoTron action 才能進 queue。
- OpenClaw/Hermes embedding 優先呼叫 Ollama `/api/embed`,只在舊節點不支援時 fallback `/api/embeddings`timeout 由 `EMBEDDING_TIMEOUT` / `OLLAMA_EMBED_TIMEOUT` 控制。
- PPT 自動產線由 `momo-scheduler` 依節奏執行 `run_ppt_auto_generation_task(schedule_kind)`:每日 20:30 產日報、週一 20:40 產週報/市場情報、每月 1 日 20:50 產月報與管理型簡報、季初 21:00 產季報、半年初 21:10 產半年報、年初 21:20 產年報,再交給 22:00 `ppt_vision_audit` 做視覺審核;每次嘗試會寫入 `ppt_generation_runs``/observability/ppt_audit_history` 以精準參數檢查目標版本是否已產生,並可用 `/observability/ppt_audit/generate_missing` 手動補齊缺漏,總開關為 `PPT_AUTO_GENERATION_ENABLED`。PPT vision 需 `PPT_VISION_ENABLED=true` 與容器內 LibreOffice`/observability/ppt_audit_file/<filename>` 會把 PPTX 轉成 PDF 快取供站內線上預覽,原始 PPTX 仍保留下載。
---
## 二、真實資料庫 Schema已校對確認
### 2.1 `products` 表SQLAlchemy ORMSQLite/PostgreSQL 通用)
| 欄位 | 型別 | 說明 |
|------|------|------|
| `id` | Integer PK | 主鍵 |
| `i_code` | String(50) UNIQUE | **MOMO 商品代碼**(爬蟲來源,即商品 SKU |
| `name` | String(255) | 商品名稱 |
| `url` | String(500) | MOMO 商品頁 URL |
| `image_url` | Text | 商品圖片 URL |
| `category` | String(100) | 分類名稱(直接欄位) |
| `status` | String(20) | 預設 `'ACTIVE'` |
| `created_at` | DateTime | 建立時間 |
| `updated_at` | DateTime | 更新時間 |
| `category_id` | Integer FK → categories.id | 分類關聯(可選) |
> **重要**: `i_code` = MOMO 網站上的商品 ID例如 `I132467614`
### 2.2 `price_records` 表
| 欄位 | 型別 | 說明 |
|------|------|------|
| `id` | Integer PK | 主鍵 |
| `product_id` | Integer FK → products.id | 商品關聯 |
| `price` | Float | **MOMO 自家售價**(爬蟲抓取) |
| `timestamp` | DateTime indexed | 抓取時間戳 |
> ⚠️ **架構限制**: `price_records` **只存 MOMO 自家售價**,無 `source` 欄位無競品PChome價格。
> PChome 比價資料必須由外部爬蟲即時抓取,以 `pchome_prices: dict` 形式注入 `HermesAnalystService.run()`。
### 2.3 `daily_sales_snapshot` 表(動態表,從 Excel 匯入)
> **重要**: 此表由 `import_service.py` 使用 `df.to_sql()` 動態建立。
> 欄位名稱**完全繼承自匯入的 MOMO Excel 報表原始欄位**,加上程式碼追加的 `snapshot_date`。
#### 已確認的關鍵欄位(實際 MOMO 報表欄位名稱)
| 欄位 | 型別 | 說明 | 備注 |
|------|------|------|------|
| `snapshot_date` | Date | 資料所屬日期(程式追加) | 由 `import_service.py` 從「日期」欄位解析 |
| `商品ID` | VARCHAR | **商品識別碼**= `products.i_code` | ⚠️ 非 `商品編號`|
| `商品名稱` | TEXT | 商品名稱 | |
| `銷售金額` | NUMERIC | 銷售業績金額 | 系統以 find_col 模糊比對,優先 `銷售金額` |
| `數量` | NUMERIC | 銷售數量 | |
| `總成本` | NUMERIC | 成本 | |
| `廠商名稱` | VARCHAR | 廠商名稱 | |
| `商品館` / `館別` | VARCHAR | 分類 | |
#### 欄位自動偵測邏輯(`find_col`
系統使用 keyword 模糊比對,**不要求欄位名完全固定**
```python
SKU/商品ID = find_col(['商品ID', 'Product ID', 'ID', 'i_code', 'Item Code'])
商品名稱 = find_col(['商品名稱', '品名', 'Name', 'Product'])
銷售金額 = find_col(['銷售金額', '業績', '金額', 'Amount', 'Sales', 'Total'])
成本 = find_col(['成本', 'Cost', '進價', '總成本'])
數量 = find_col(['銷售數量', '銷量', '數量', 'Qty', 'Quantity'])
日期 = find_col(['日期', '訂單日期', '交易日期', 'Date'])
分類 = find_col(['商品館', '館別', '分類', 'Category'])
```
### 2.4 `competitor_prices` 表Migration 004 — 已建立)
競品價格快取表,由 `competitor_price_feeder.py` Worker 寫入AI Pipeline LEFT JOIN 消費。
| 欄位 | 型別 | 說明 |
|------|------|------|
| `id` | SERIAL PK | 主鍵 |
| `sku` | VARCHAR(50) | MOMO 商品代碼(= products.i_code |
| `source` | VARCHAR(30) | 競品來源:`'pchome'`(預留 shopee 等) |
| `price` | NUMERIC(10,2) | 競品售價 |
| `original_price` | NUMERIC(10,2) | 競品原價 |
| `discount_pct` | INTEGER | 折扣 %NULL=未折扣) |
| `competitor_product_id` | VARCHAR(100) | PChome 商品 ID |
| `competitor_product_name` | TEXT | PChome 商品名稱(核對用) |
| `match_score` | NUMERIC(4,3) | 商品身份比對分數0~1< 0.76 不寫入正式快取 |
| `tags` | JSONB | 語意標籤,如 `["on_sale","discount_20pct"]` |
| `crawled_at` | TIMESTAMP | 爬取時間 |
| `expires_at` | TIMESTAMP | TTL = crawled_at + 6h過期後 Hermes 忽略 |
**UNIQUE**: `(sku, source)` — 同一 SKU+來源只有一筆ON CONFLICT UPDATE
**語意標籤字典**
| 標籤 | 觸發條件 |
|------|---------|
| `on_sale` | PChome `is_on_sale = True` |
| `discount_10pct` | 折扣 10-19% |
| `discount_20pct` | 折扣 20-29% |
| `discount_30pct` | 折扣 ≥ 30% |
| `low_stock` | 庫存 < 10 |
| `high_rating` | 評分 ≥ 4.5 |
### 2.5 `ai_price_recommendations` 表Migration 003 — 已建立)
此表需執行 `migrations/003_ai_price_recommendations.sql` 才能完整寫入 DB
```sql
CREATE TABLE IF NOT EXISTS ai_price_recommendations (
id SERIAL PRIMARY KEY,
sku VARCHAR(50) UNIQUE NOT NULL,
name TEXT NOT NULL,
reason TEXT,
status VARCHAR(20) DEFAULT 'pending', -- pending / approved / rejected
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
> **現狀**: `nemoton_dispatcher_service.py` 的 `_exec_add_to_recommendation()` 在 engine 注入且表存在時才寫入,否則只發 Telegram 通知,不會 crash。
---
## 三、SQL 漏斗設計(已修正欄位名稱)
`hermes_analyst_service.py``fetch_candidates()` 的核心 SQL
```sql
WITH latest_momo_price AS (
-- 從爬蟲商品庫取最新 MOMO 售價
SELECT
p.i_code AS sku, -- MOMO 商品代碼
p.name,
p.category,
pr.price AS momo_price,
ROW_NUMBER() OVER (PARTITION BY p.id ORDER BY pr.timestamp DESC) AS rn
FROM products p
JOIN price_records pr ON pr.product_id = p.id
WHERE p.status = 'ACTIVE'
),
recent_sales AS (
-- 從每日業績快照計算近7天 vs 前7天銷售額
SELECT
"商品ID" AS sku, -- ⚠️ 實際欄位名為「商品ID」(非「商品編號」)
SUM(CASE WHEN snapshot_date >= CURRENT_DATE - 7
THEN COALESCE("銷售金額"::numeric, 0) ELSE 0 END) AS sales_7d_curr,
SUM(CASE WHEN snapshot_date >= CURRENT_DATE - 14
AND snapshot_date < CURRENT_DATE - 7
THEN COALESCE("銷售金額"::numeric, 0) ELSE 0 END) AS sales_7d_prev
FROM daily_sales_snapshot
GROUP BY "商品ID"
)
SELECT lmp.sku, lmp.name, lmp.category, lmp.momo_price,
rs.sales_7d_curr, rs.sales_7d_prev
FROM latest_momo_price lmp
JOIN recent_sales rs ON rs.sku = lmp.sku
WHERE lmp.rn = 1
AND rs.sales_7d_prev > 0
AND (rs.sales_7d_curr - rs.sales_7d_prev) / rs.sales_7d_prev < -0.10
ORDER BY (rs.sales_7d_curr - rs.sales_7d_prev) / rs.sales_7d_prev ASC
LIMIT 300
```
**漏斗效果**: 226萬筆 price_records → ~300 筆近7天銷量跌幅 > 10% 的活躍商品)
**JOIN 邏輯**:
- `products.i_code``daily_sales_snapshot."商品ID"` — 均為 MOMO 商品代碼,格式相同
---
## 四、競品價格補給線架構(已實裝)
### 生產者-消費者解耦設計
```
[competitor_price_feeder.py Worker] ←← 每 4 小時獨立運行
↓ 搜尋 PChomesearch_products
↓ 商品身份比對marketplace_product_matcher.py
↓ 提取語意標籤
↓ UPSERT competitor_pricesTTL 6h
[HermesAnalystService.fetch_candidates()] ←← AI Pipeline 消費端
↓ LEFT JOIN competitor_prices零網路等待
↓ 有效期內expires_at > NOW()+ match_score ≥ 0.76 + tags 含 identity_v2 才 JOIN
↓ pchome_price + competitor_tags 一起傳給 Hermes
```
### 關鍵設計決策
| 決策 | 選擇 | 原因 |
|------|------|------|
| 解耦方式 | DB 表快取(非 Redis | PostgreSQL 已是核心,無需額外依賴;支援 JOIN |
| TTL | 6 小時 | 與 AI Pipeline 排程週期對齊 |
| 比對算法 | 品牌 + 核心 token + 容量/重量/包數 + 品類 + 價格 sanity check | 由 `marketplace_product_matcher.py` 統一供 feeder、legacy crawler、AI/PPT 鏈路使用 |
| 最低比對門檻 | 0.76 | 核心比價寧可待審,不允許低信心錯配影響 AI 決策 |
| 已有不同 PChome 商品覆蓋門檻 | 0.84 | 新候選與既有正式配對不同時,除非超高信心,否則寫入 `needs_review` attempt 不覆蓋 |
| 單位價可比模式 | `unit_comparable` | 同核心商品但買送/套組/件數不同時,不寫正式總價差;只寫入 attempt供單位價或人工覆核 |
| 語意標籤 | JSONB 陣列 | 傳給 Hermes 提升情境感知品質 |
### 競品比對邏輯(`competitor_price_feeder.py`
```
MOMO 商品名稱
→ marketplace_product_matcher.build_search_terms()
→ PChomeCrawler.search_products(keyword, limit=12)
→ marketplace_product_matcher.score_marketplace_match()
→ 品牌衝突 / 容量衝突 / 包數衝突 hard veto
→ 同核心但買送/套組/件數不同標記 unit_comparable不進正式總價差
→ 同款高信心 score ≥ 0.76 才進 competitor_prices
→ 低信心、規格衝突、既有配對衝突寫入 competitor_match_attempts
```
### `fetch_candidates()` v2 漏斗(已更新)
```sql
LEFT JOIN competitor_prices cp
ON cp.sku = lmp.sku
AND cp.source = 'pchome'
AND cp.expires_at > NOW()
AND cp.match_score >= 0.76
AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2'
```
→ 無競品資料的商品仍回傳,`pchome_price=NULL``_batch_analyze` 自動跳過
### 下游消費規範2026-05-19 更新)
- Dashboard、AI pick、Hermes、Excel export、daily/growth 圖表與 competitor PPT 必須以 `competitor_prices + competitor_price_history + competitor_match_attempts` 為短期唯一生產真相源,且只消費 `identity_v2` matcher 驗證過的配對;舊版僅靠 `match_score` 的快取不可直接進入決策或簡報。
- `pchome_matches` 與 live `pchome_batch()` 僅保留 legacy compatibility不得作為新簡報或 AI 決策主來源。
- `services/competitor_intel_repository.py` 是下游頁面、圖表、簡報的共用查詢出口;新增消費端不得各自硬寫不同 match threshold。
- competitor PPT 不可只輸出 matched rows 造成覆蓋率假象;`fetch_competitor_comparison_results()` 必須用 `LEFT JOIN valid_competitor` 保留高營收/高價但尚未有效配對的 MOMO 商品,並帶出 `match_status``candidate_count``best_match_score``match_diagnostic`,讓簡報與 AI 文案明確區分「高信心比對」與「待補身份/價格」。
- `services/competitor_identity_revalidator.py` 可對既有 `competitor_prices` legacy row 離線重跑 `identity_v2`:只有新版 matcher 分數 `>= 0.76` 且無 hard veto 才補 `identity_v2` / `legacy_revalidated` tags預設不刷新 `expires_at`,避免過期價格進入決策。
- `CompetitorPriceFeeder.run_expired_identity_refresh()` 會優先刷新已通過 `identity_v2` 但 TTL 過期的 PChome row直接用既有 `competitor_product_id` 批次呼叫 PChome 商品 API再用新版 matcher 重新驗證名稱/規格/價格 sanity通過後寫回 `competitor_prices``competitor_price_history`。這條路徑提升新鮮價格覆蓋率,但不降低 match threshold也不讓過期價格直接進入決策。
- `marketplace_product_matcher.py` 的擴充只能走「正向證據 + 反向 veto」品牌一致、商品線/型號訊號強、價格合理且無 hard veto 時才允許 `strong_product_line_match` 加分;補充瓶/補充包/refill 與一般正裝不互相配對,分享組/加量組/明星組等組合包不得誤配單品。
- 套組/買送/件數不同但品牌、核心商品線與單一基礎規格一致時matcher 必須回傳 `comparison_mode='unit_comparable'``unit_comparable` reasonFeeder 只能寫入 `competitor_match_attempts.attempt_status='unit_comparable'``refresh_unit_comparable`,不得寫入 `competitor_prices`,直到下游支援單位價換算或人工覆核。若任一側含多個不同容量/重量規格,視為多品項套組,不可進 `unit_comparable`
- PChome feeder 的外部 request timeout 由 `PCHOME_FEEDER_TIMEOUT` 控制,預設 12 秒;排程不得因單一 PChome 搜尋 API timeout 被拖到數分鐘。
- 商品看板的 PChome 狀態必須把 matcher 診斷原因翻成可行動語意:品牌衝突、規格衝突、補充包差異、組合差異、商品線不符等,不可只顯示籠統「待比對」或「身份否決」。
- Dashboard 必須把「待比對」拆成可診斷狀態:`價格過期待刷新``舊版配對待重驗``低分配對待審``身份否決``需單位價比較``找不到同款``抓取異常``尚未搜尋`。不可再用單一「待比對」掩蓋資料品質原因。
### 執行方式
```bash
# 手動觸發一輪抓取
python3 services/competitor_price_feeder.py
# 預覽 legacy PChome 快取 identity_v2 重驗證(不寫入)
python3 -m services.competitor_identity_revalidator --limit 500
# 寫入安全通過的 identity_v2 tag不刷新過期價格
python3 -m services.competitor_identity_revalidator --limit 500 --apply
# 未來整合為 K3s CronJob每 4 小時)
# k8s/jobs/competitor-price-feeder-cronjob.yaml
```
---
## 五、Telegram 語意化訊息規範ChatOps 標準)
### 5.1 核心原則
1. **語意化排版 (Semantic Formatting)** — Emoji 作為視覺標籤0.5 秒判斷嚴重性
2. **倒金字塔結構** — 結論先行 → 核心數據 → AI 洞察 → 建議行動 → 運算足跡
3. **收斂行動呼籲 (Call to Action)** — 每則訊息只有一個明確的 👉 建議行動
4. **底部運算足跡** — FinOps + Observability用分隔線隔開主訊息
5. **EA HITL 專業 brief**`ea_escalation` 必須分成決策狀態、背景摘要、風險摘要、TOP 待審 SKU 與建議處置;價格類行動不得用長 bullet 串接,必須拆出 MOMO/PChome 價格、價差、人工處置與 PChome ID。
### 5.2 語意化 Emoji 字典
| 類別 | Emoji | 語意 |
|------|-------|------|
| 身份識別 | `⚡ NemoTron 派發器` | Dispatcher 身份 |
| 身份識別 | `🔍 Hermes 3 8B` | Analyst 身份(僅出現在足跡) |
| 風險級別 | `🚨` | 高危險,立即行動 |
| 風險級別 | `⚠️` | 中風險,人工覆核 |
| 風險級別 | `💡` | 低風險,策略建議 |
| 例行報告 | `📊` | 核心數據區塊標頭 |
| 業務屬性 | `💰` | 價格/毛利 |
| 業務屬性 | `📦` | 庫存/銷量 |
| 業務屬性 | `🏆` | 競品情報 |
| AI 洞察 | `🧠` | AI 分析結果 |
| 運算足跡 | `⚙️` | FinOps 底部區塊 |
### 5.3 三大類訊息模板(標準格式)
#### 類別一:緊急告警(`trigger_price_alert` 觸發)
```
🚨 [⚡ NemoTron 派發器] 競價高危險預警
⚠️ 核心問題:[A003 舒特膚 AD 乳液] 價格大幅落後競品,訂單流失中!
📊 關鍵數據:
• 我方價格:$1,200
• 競品價格:$980 (價差 22.4%)
• 銷量變化:近七天銷量 -35.0%
🧠 AI 洞察 (信心度 85%)
價差已突破 20% 警戒線,且伴隨實質銷量下滑,高度判定為競品大力促銷攔截。
👉 建議行動:建議立即降價至 $1,000 迎戰,或發放 $200 專屬折價券
─────────────────────
⚙️ 運算足跡:
• 🔍 分析: Hermes 3 8B (GCP-A/GCP-B/111 Ollama) | 耗時: 34.2s | Tokens: 512 | $0 成本
• ⚡ 決策: NemoTron NIM | 185 Tokens | $0 (配額內 2/80)
```
#### 類別二:人工覆核(`flag_for_human_review` 觸發)
```
⚠️ [⚡ NemoTron 派發器] 異常波動需人工覆核
🔍 待查商品:[A001 玻尿酸面膜10片裝]
📊 矛盾數據:
• 價格狀態:無明顯價差 (與競品齊平)
• 異常現象:過去 3 天銷量突然掛零 (平日日均 15 件)
• 庫存狀態:目前庫存充足 (500+ 件)
🧠 AI 洞察 (信心度 45%)
數據出現矛盾訊號AI 信心不足以自主決策,需人工走查確認。
👉 建議行動:請營運人員立即進行人工走查。
─────────────────────
⚙️ 運算足跡:
• 🔍 分析: Hermes 3 8B (GCP-A/GCP-B/111 Ollama) | 耗時: 34.2s | Tokens: 512 | $0 成本
• ⚡ 決策: NemoTron NIM | 185 Tokens | $0 (配額內 2/80)
```
#### 類別三:策略執行通知(`add_to_recommendation` 觸發)
```
💡 [⚡ NemoTron 派發器] 潛力商品自動佈署
🏆 推薦品項:[A009 美白化妝水150ml] 已自動加入「首頁推薦區塊」
📊 決策依據:
我方價格低於市場 20%近7天銷量回升具備流量轉換潛力
🧠 AI 洞察 (信心度 82%)
具備價格競爭優勢NemoTron 主動提升曝光量以最大化業績。
👉 執行狀態:✅ 系統已自動寫入 ai_price_recommendations 推薦表
─────────────────────
⚙️ 運算足跡:
• 🔍 分析: Hermes 3 8B (GCP-A/GCP-B/111 Ollama) | 耗時: 34.2s | Tokens: 512 | $0 成本
• ⚡ 決策: NemoTron NIM | 185 Tokens | $0 (配額內 2/80)
```
#### 類別四Gemini 備援 / 鎖定場景推理週報
```
... (前文省略) ...
─────────────────────
⚙️ 運算足跡:
• 🔍 彙整: Hermes 3 8B (GCP-A/GCP-B/111 Ollama) | 耗時: 12s | $0 成本
• 🧠 備援/鎖定場景: Gemini 2.5 Flash | 8,420 Tokens | 費用: 約 $0.003 USD
```
### 5.4 運算足跡資料來源
| 模型 | API Response 欄位 | 說明 |
|------|-----------------|------|
| Hermes (Ollama) | `eval_count`, `total_duration` | 生成 tokens 數 + 推理耗時 (ns→s) |
| NemoTron (NIM) | `usage.total_tokens` (OpenAI 格式) | prompt + completion tokens 合計 |
| Gemini | `usageMetadata.totalTokenCount` | 乘費率算 USD |
> **程式碼位置**: `nemoton_dispatcher_service.py` → `_build_footprint_block(hermes_stats, nim_stats)`
### 5.5 Inline Keyboard 按鈕Level 2 互動,待實裝)
當收到類別一緊急告警時訊息底部附帶互動按鈕Telegram Bot API inline_keyboard
```
[ ✅ 批准降價 ] [ ❌ 拒絕並忽略 ] [ 🔗 查看報表 ]
```
- `✅ 批准降價` → 呼叫 MOMO PRO 後台 API 改價 + 決策寫入知識庫
- `❌ 拒絕並忽略` → 決策寫入知識庫(訓練未來在此品類保守點)
- `🔗 查看報表` → 跳轉至 MOMO PRO 該商品數據分析頁
> **現狀**: 尚未實裝Inline Keyboard 需搭配 Telegram Webhook + callback_query handler
---
## 六、Telegram 告警架構
### 告警群組
- 群組: **小龍蝦** (業務情報專用,非 SRE 維運)
- Chat ID: `-1003940688311`
- Bot: `8610496165:AAFOlcWV4oRUSC2TI-fYux7JV97fjNzsYR8`
### 單 Bot 多身份策略One Bot, Multiple Headers
| 模組 | Telegram 標頭 |
|------|--------------|
| Hermes 分析師 | `[Hermes 分析師]` |
| NemoTron 派發器 | `[NemoTron 派發器]` |
| Gemini 備援 | `[Gemini 備援]`(僅 Ollama 失敗或 ADR-028 鎖定場景) |
### 三種告警類型
| Tool | 觸發條件 | Telegram 格式 |
|------|---------|--------------|
| `trigger_price_alert` | HIGH 風險 (gap>15% + 銷量跌>20%) | 🔴/🟡 競價威脅告警 |
| `add_to_recommendation` | 我方價格低於競品且銷量正成長 | ⭐ 推薦商品候選 |
| `flag_for_human_review` | 信心 < 0.6 或情況複雜 | ⚠️ 需要人工審核 |
---
## 六、已驗證的服務參數
### Hermes 分析師
| 參數 | 值 |
|------|---|
| 模型 | `hermes3:latest` |
| Ollama URL | GCP-A `http://34.143.170.20:11434` → GCP-B `http://34.21.145.224:11434` → 111 `http://192.168.0.111:11434` |
| Timeout | 120s |
| Temperature | 0.1 |
| 實測推理時間 | **19.3s3筆實彈 2026-04-17** |
| TOP_N | 20每次最多輸出威脅數 |
### NemoTron 派發器
| 參數 | 值 |
|------|---|
| 模型 | `meta/llama-3.1-8b-instruct` |
| NIM API URL | `https://integrate.api.nvidia.com/v1` |
| Timeout | 60s |
| 每日配額上限 | 80留 20 給 AWOOOI |
| 配額耗盡 fallback | 直接派發 HIGH 風險告警,跳過 NIM |
| 實測 token 消耗 | **1206 tokens/輪(實彈 2026-04-17** |
---
## 七、已知技術債與待辦
| 優先 | 項目 | 說明 |
|------|------|------|
| ✅ | Migration 003 + 004 | `competitor_prices` + `ai_price_recommendations` 已在 188 `momo_analytics` 執行 |
| ✅ | 188 生產容器實彈驗證 | Hermes + NIM + PostgreSQL + Telegram 全通 (2026-04-17) |
| ✅ | momo-app port 5001→5002 | docker-registry 佔用 5001docker-compose.yml 改為 `127.0.0.1:5002:80`,已 Up healthy |
| ✅ | momo-app 雙網路連線 | 同時連 `momo-network` + `momo-pro_default`(後者含 `momo-db` alias `momo-postgres`|
| P1 | PChome Feeder CronJob | `competitor_price_feeder.py` 每 4 小時排程 (Scheduler 整合) |
| P1 | 告警去重 TTL | 同一 SKU 短期內重複告警未防範 |
| P1 | `daily_sales_snapshot` 欄位防禦 | 若 Excel 欄位名變更JOIN 條件會靜默失效 |
| P2 | Scheduler 整合 | 每6小時自動觸發 Hermes→NIM→Telegram 管線 |
| P2 | Gemini 備援治理 | 僅保留 ADR-028 鎖定場景與 Ollama 失敗備援,新增 caller 必須走 ADR |
---
## 八、部署拓撲2026-04-17 確認)
### 實體機器對應
| 服務 | 主機 | 容器名 | 說明 |
|------|------|--------|------|
| PostgreSQL | 192.168.0.188 | `momo-db` | pgvector/pgvector:pg14含所有 AI 相關表 |
| momo-app | 192.168.0.188 | `momo-pro-system` | **Up healthyport 5002:80**5001 被 docker-registry 佔用,已改 5002 |
| momo-scheduler | 192.168.0.188 | `momo-scheduler` | 常駐排程容器 |
| Ollama Primary | 34.143.170.20 | Ollama 原生 | GCP-AAI/LLM/embedding 主路徑 |
| Ollama Secondary | 34.21.145.224 | Ollama 原生 | GCP-B同等備援 |
| Ollama Fallback | 192.168.0.111 | Ollama 原生 | 最後一道本地防線 |
| E2E 驗證容器 | 192.168.0.188 | `momo-e2e-test` | 臨時容器,含新服務模組 |
### 188 `/home/ollama/momo-pro/.env` 正確設定
```bash
TELEGRAM_BOT_TOKEN=8610496165:AAFOlcWV4oRUSC2TI-fYux7JV97fjNzsYR8 # ← 唯一正確 token
TELEGRAM_CHAT_IDS=["-1003940688311"] # 小龍蝦群組
NVIDIA_API_KEY=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
USE_POSTGRESQL=true
POSTGRES_HOST=momo-db
# POSTGRES_DB / USER / PASSWORD 使用 docker-compose.yml 預設值
```
> ⚠️ **Split-brain 陷阱**188 容器環境曾存在舊 bot token `8569720657`(不同 bot不在小龍蝦群組
> 已於 2026-04-17 深夜修正為正確 token `8610496165`。
---
## 九、校對歷程
| 日期 | 問題 | 修正 |
|------|------|------|
| 2026-04-17 | `fetch_candidates()` SQL 使用 `"商品編號"` | 修正為 `"商品ID"`(與 MOMO Excel 實際欄位名一致) |
| 2026-04-17 | Hermes gap_pct 由 LLM 計算 → 誤差大 | 改為 Python 預算 `(momo-pchome)/pchome*100` |
| 2026-04-17 | 推理時間 52s | 預算 gap_pct 後降至 19.3s (3筆) |
| 2026-04-17 | `model_footprint` DB 欄位寫入 `{}` | 分離 `footprint_text`Telegram 顯示)與 `footprint_data`DB JSON|
| 2026-04-17 | SQLite 語法 `NOW()` / `datetime('now')` / `::jsonb` | 全部改為 `CURRENT_TIMESTAMP` / Python 計算(跨 DB 相容)|
| 2026-04-17 | 本機 SQLite 測試通過但 188 未同步任何檔案 | rsync 推送 6 核心檔案 + 全站 dry-run 對帳 + migrations 跑通 |
| 2026-04-17 | 188 容器無 volume mount`docker cp` 臨時解 | 重建 image`COPY . .` bake 進新代碼port 5001 衝突記錄為技術債 |
| 2026-04-17 | 188 .env Telegram token 不正確split-brain| 修正為 `8610496165`188→Telegram message_id=282 確認 |
| 2026-04-17 | NIM Tool Calling E2E | 真實 NVIDIA_API_KEY 驗證dispatched=3, errors=[] |