對齊 Ollama-first flag 語義
All checks were successful
CD Pipeline / deploy (push) Successful in 56s

This commit is contained in:
OoO
2026-05-13 12:00:21 +08:00
parent ce208921af
commit 6313fdd293
6 changed files with 43 additions and 32 deletions

View File

@@ -51,6 +51,7 @@
- `services/pg_sync_service.py` 是顯式 opt-in legacy CLI不是生產自動同步路徑`tests/test_pg_sync_contract.py` 已守住預設 OFF 與 runtime paths 不自動 import。
- `qwen3:14b` 不是未使用 Ollama 模型OpenClaw QA、NemoTron dispatch 與 LLM model router 仍有現役路徑;`tests/test_qwen3_runtime_usage.py` 已守住,不能只因體積大就三主機移除。
- Ollama host env 已加白名單護欄:`OLLAMA_HOST*` / `EMBEDDING_HOST` 只接受 GCP-A、GCP-B、111 或 110 proxy誤設 188/localhost 會回到核准主機。
- OpenClaw QA / daily Hermes template / NemoTron qwen3 的 flag 文件與測試已對齊 Ollama-first 預設 ON顯式 `false` 才是 Gemini/NIM legacy 緊急退路。
- `routes/price_comparison_routes.py` 的 MOMO crawler TODO 已接到既有 `services.momo_crawler.search_momo_products()`;未手動上傳 MOMO 商品時會自動抓 MOMO再交給比價服務。
- Telegram `momo:eig:<event_id>` callback 已在 `routes/openclaw_bot_routes.py``services/telegram_bot_service.py` 實作並有 webhook 測試覆蓋,不是未實作缺口。
- Telegram `date_*` / `goal_*` 不是死 callback handler按鈕先送 `await:*` 進入輸入等待狀態,使用者下一則文字才由 pending action 消費;`tests/test_openclaw_bot_menu_keyboards.py``tests/test_openclaw_bot_routes_webhook.py` 已覆蓋。

View File

@@ -110,8 +110,8 @@ _nim_call_count = {"date": "", "count": 0}
# ── Operation Ollama-First v5.0 / Phase 3 / A9 ──────────────────
# GCP Ollama qwen3:14b 灰度切換開關
# - 預設 false → 行為與戰前完全相同(仍走 NIM
# - true → qwen3 主路徑NIM 降為備援,最後仍兜底 Hermes 規則引擎ADR-004
# - false → 緊急停用 Ollama-first 時才回 NIM-first
# 模型選擇A2 web-research 紅綠燈報告 docs/phase0_research_report_20260503.md
# 原戰役計畫 deepseek-r1:14b 的 Ollama tool_calls chat template 缺對應 jinja
# GitHub Issue #10935 未解),改採 qwen3:14bOllama 官方 + qwenlm 雙確認 tools 支援)。
@@ -1397,7 +1397,7 @@ class NemotronDispatcher:
}
# ── Operation Ollama-First v5.0 / Phase 3 / A9qwen3 主路徑feature flag 灰度)──
# 預設 NEMOTRON_OLLAMA_FIRST=false 時不進入此分支,行為與戰前完全相同
# NEMOTRON_OLLAMA_FIRST=false 時不進入此分支,僅作緊急退路
# 若 qwen3 成功取得 tool_calls沿用既有 TOOL_MAP 執行邏輯(共用 footprint/threat 注入)。
# 若 qwen3 失敗或 0 tool_calls → 不直接降到 Hermes 規則,先嘗試 NIM 備援,再走 ADR-004。
qwen3_used = False
@@ -1440,7 +1440,7 @@ class NemotronDispatcher:
pre_errors=errors,
)
# ── 進入 NIM 路徑flag=false 預設主路徑flag=true 則為 qwen3 失敗備援)──
# ── 進入 NIM 路徑flag=false 緊急主路徑flag=true 則為 qwen3 失敗備援)──
if not NIM_API_KEY:
logger.warning("[Dispatcher][ADR-004] NVIDIA_API_KEY 未設定,啟動 Hermes 規則引擎降級")
fb = self._hermes_rule_fallback(nim_candidates, hermes_stats)

View File

@@ -48,12 +48,12 @@ NVIDIA_FALLBACK_MODEL = "meta/llama-3.3-70b-instruct"
TAIPEI_TZ_OFFSET = 8 # UTC+8
# ──────────────────────────────────────────────────────────────────────────────
# Operation Ollama-First v5.0 — Phase 3 feature flag (預設 OFF統帥手動開灰度)
# - OPENCLAW_QA_OLLAMA_FIRST: false=維持戰前 Gemini-first 行為,true=走 Ollama 主、Gemini fallback
# Operation Ollama-First v5.0 — Phase 3 feature flag預設 ONGemini 僅 fallback
# - OPENCLAW_QA_OLLAMA_FIRST: true=走 Ollama 主、Gemini fallbackfalse=緊急退回 legacy Gemini-first
# - OPENCLAW_QA_OLLAMA_MODEL: GCP Ollama 上的模型 tagA2 推薦 qwen3:14b9.3GB
# - OPENCLAW_QA_OLLAMA_HOST: 允許獨立指定 QA 用主機;未設則 fallback 到通用 OLLAMA_HOST_PRIMARY
# - OPENCLAW_QA_OLLAMA_TIMEOUT: 單次 Ollama 呼叫超時(秒),低品質判定後仍會升級 Gemini
# 任何 deploy 不開 flag → 行為與戰前完全相同regression-safe
# 任何 deploy 不開 flag → Ollama-first緊急時才顯式設 false 回 legacy
# ──────────────────────────────────────────────────────────────────────────────
@@ -163,8 +163,8 @@ def generate_strategy_response(query: str, context: Optional[Dict[str, Any]] = N
繁體中文回覆字串。所有 LLM 失敗時回降級訊息(永遠回字串、不拋例外)。
路由Operation Ollama-First v5.0 — Phase 3:
OPENCLAW_QA_OLLAMA_FIRST=false預設→ Gemini → NIM(戰前行為)
OPENCLAW_QA_OLLAMA_FIRST=true GCP Ollama qwen3:14b → 品質檢測 → fallback Gemini → NIM
OPENCLAW_QA_OLLAMA_FIRST=true預設 GCP Ollama qwen3:14b → 品質檢測 → fallback Gemini → NIM
OPENCLAW_QA_OLLAMA_FIRST=false緊急停用時才回 Gemini → NIM legacy 路徑
"""
q = (query or "").strip()
if not q:
@@ -174,7 +174,7 @@ def generate_strategy_response(query: str, context: Optional[Dict[str, Any]] = N
# ── Phase 11 RAG-firstfeature flag 預設 OFF只在 Q&A 入口接,週月年報不接)──
# 高信心 RAG 命中 → 直接回 ai_insights 內容,避免 LLM 呼叫
# 低信心或 flag OFF → 走既有 Ollama → Gemini → NIM 路徑
# 低信心或 flag OFF → 走後續 Ollama-first / fallback 路徑
if is_rag_enabled():
try:
rag = rag_service.query(
@@ -191,7 +191,7 @@ def generate_strategy_response(query: str, context: Optional[Dict[str, Any]] = N
except Exception as exc:
logger.warning("[OpenClaw][QA] RAG query failed (%s), fallback LLM", exc)
# ── 灰度路徑Ollama 優先flag=true 才走,預設 OFF)──
# ── 路徑Ollama 優先flag=true預設 ON)──
if _qa_ollama_first_enabled():
ollama_reply = _call_qwen3_qa(q, context, request_id)
if ollama_reply and not _is_low_quality_response(ollama_reply):
@@ -202,7 +202,7 @@ def generate_strategy_response(query: str, context: Optional[Dict[str, Any]] = N
request_id,
)
# ── 既有路徑Gemini → NIMA4 已接 ai_call_logger──
# ── 備援路徑Gemini → NIMA4 已接 ai_call_logger──
return _legacy_gemini_first_qa(q, context, request_id=request_id)
@@ -211,7 +211,7 @@ def _legacy_gemini_first_qa(
context: Optional[Dict[str, Any]],
request_id: Optional[str] = None,
) -> str:
"""戰前 Gemini-first 路徑;抽出獨立函式以利 Phase 3 灰度與 regression test"""
"""legacy Gemini-first 緊急退路;正常情況只在 Ollama 主路徑失敗後使用"""
system_prompt = (
"你是 MOMO Pro 電商情報策略師「OpenClaw」。以繁體中文台灣用語回覆使用者。"
"嚴禁簡體字,嚴禁空洞套話。若使用者要求的資料需即時查詢,"
@@ -1482,7 +1482,7 @@ def _daily_hermes_template_enabled() -> bool:
"""Operation Ollama-First v5.0 Phase 3 — Hermes 模板模式 feature flag.
每次呼叫即時讀取,允許 runtime toggle 灰度(不需重啟 scheduler
預設 false → 走 _legacy_full_gemini_daily_report(戰前行為,零 regression
預設 true → Hermes/Ollama 模板模式;false 才回 _legacy_full_gemini_daily_report。
"""
# 統帥 2026-05-03 23:30 指令:「免費優先」
# 預設 ONHermes 算 KPI + 模板填充Gemini 只寫 200 字洞察(戰前 28K → ~8K tokens, -71%
@@ -1495,9 +1495,9 @@ def generate_daily_report() -> dict:
OpenClaw 電商日報(每日 09:00— Operation Ollama-First v5.0 Phase 3 路由層。
依 ``OPENCLAW_DAILY_HERMES_TEMPLATE`` 分流:
- false預設``_legacy_full_gemini_daily_report``Gemini 全文寫稿(~28K tokens
- true``_generate_daily_report_hermes_template``Hermes 算 KPI + 模板填充 +
- true預設``_generate_daily_report_hermes_template``Hermes 算 KPI + 模板填充 +
Gemini 寫 200 字洞察(~8K tokens, -71%
- false``_legacy_full_gemini_daily_report``Gemini 全文寫稿(~28K tokens
回傳合約兩條路徑一致:``{status, report_type, insight_id, period, ...}``
cron 不需修改ai_insights schema 不變(仍 type='daily_report')。

View File

@@ -9,7 +9,7 @@ Operation Ollama-First v5.0 / Phase 3 / A9 — Nemotron qwen3 切換相容性測
T3. qwen3 同時回 tool_calls + content → 優先採用 tool_calls
T4. qwen3 連線失敗 → 不丟例外給上游,自動 fallback NIM 路徑
T5. qwen3 + NIM 都失敗 → ADR-004 走 Hermes 規則引擎降級(含「🟡 [規則引擎]」標記)
T6. NEMOTRON_OLLAMA_FIRST 預設 false → 完全不呼叫 qwen3(戰前行為)
T6. NEMOTRON_OLLAMA_FIRST=false → 緊急退回 NIM-first不呼叫 qwen3
紀律:
- 所有 HTTP 互動 mock不實際呼叫 GCP Ollama 或 NIM
@@ -365,10 +365,10 @@ def test_qwen3_and_nim_both_fail_falls_back_to_hermes_rules(monkeypatch):
# ─────────────────────────────────────────────────────────────
# T6. feature flag 預設 false → 戰前行為qwen3 完全不被呼叫
# T6. feature flag 顯式 false → 緊急退路qwen3 完全不被呼叫
# ─────────────────────────────────────────────────────────────
def test_flag_default_false_preserves_pre_war_behavior(monkeypatch):
"""NEMOTRON_OLLAMA_FIRST 預設 false 時dispatch 不應觸碰 GCP Ollama
def test_flag_false_preserves_nim_first_emergency_path(monkeypatch):
"""NEMOTRON_OLLAMA_FIRST=false 時dispatch 不應觸碰 GCP Ollama
nim_stats 不可帶 provider='gcp_ollama'"""
import services.nemoton_dispatcher_service as module

View File

@@ -4,8 +4,8 @@ tests/test_openclaw_daily_template.py
Operation Ollama-First v5.0 / Phase 3 / A8 — 日報模板路由測試
驗證面:
T1. flag=false(預設)→ 走 _legacy_full_gemini_daily_reportregression
T2. flag=true → 走 _generate_daily_report_hermes_template
T1. flag=false → 走 _legacy_full_gemini_daily_report緊急退路
T2. flag=true(預設)→ 走 _generate_daily_report_hermes_template
T3. _compute_daily_kpi 各 KPI 函數可獨立 mock 測DB 失敗回安全預設)
T4. _render_daily_template_v2 缺欄位優雅降級_SafeUndefined 不 raise
T5. _SafeUndefined 對 'X.Y.Z' 巢狀存取不爆
@@ -86,8 +86,8 @@ class TestRouting:
assert hermes_called['v'] is True, "flag=true 必須走 hermes 模板路徑"
assert legacy_called['v'] is False, "flag=true 不可走 legacy"
def test_flag_default_is_false(self, monkeypatch):
"""無 env 設定時 → 預設 false戰前行為"""
def test_flag_default_is_true(self, monkeypatch):
"""無 env 設定時 → 預設 trueHermes/Ollama-first"""
# 不 set env
import importlib
import services.openclaw_strategist_service as svc

View File

@@ -6,7 +6,7 @@ OpenClaw Q&A 路由 + 品質守門 unit tests
(Operation Ollama-First v5.0 — Phase 3, A7 fullstack-engineer)
涵蓋:
- feature flag OPENCLAW_QA_OLLAMA_FIRST=false → Gemini-firstregression test
- feature flag OPENCLAW_QA_OLLAMA_FIRST=false → 緊急回 legacy Gemini-first
- flag=true + 高品質 Ollama 回應 → 直接回 Ollama 結果,不走 Gemini
- flag=true + 低品質 Ollama 回應 → 升級至 Gemini並標 fallback_to=openclaw_qa_gemini_fallback
- flag=true + Ollama 呼叫失敗 → 升級至 Gemini
@@ -54,7 +54,7 @@ def reset_state(monkeypatch):
monkeypatch.setattr(logger_mod, '_write_to_db', fake_write)
monkeypatch.setenv('AI_CALL_LOGGING_ENABLED', 'true')
# 預設 flag=false戰前行為
# 預設 flag=trueOllama-first
monkeypatch.delenv('OPENCLAW_QA_OLLAMA_FIRST', raising=False)
yield captured
@@ -136,13 +136,13 @@ class TestLowQualityRules:
# ─────────────────────────────────────────────────────────────────────────────
# 2. Routingfeature flag = false 時維持 Gemini-first 路regression
# 2. Routingfeature flag = false 時維持 Gemini-first 緊急退
# ─────────────────────────────────────────────────────────────────────────────
class TestFlagOff:
def test_flag_false_routes_to_legacy(self, monkeypatch, reset_state):
"""flag=false(預設)→ 不應該呼叫 _call_qwen3_qa直接走 _legacy_gemini_first_qa。"""
"""flag=false → 不應該呼叫 _call_qwen3_qa直接走 _legacy_gemini_first_qa。"""
monkeypatch.setenv('OPENCLAW_QA_OLLAMA_FIRST', 'false')
legacy_called = {'count': 0}
ollama_called = {'count': 0}
@@ -163,20 +163,30 @@ class TestFlagOff:
assert legacy_called['count'] == 1
assert ollama_called['count'] == 0
def test_flag_unset_defaults_to_off(self, monkeypatch, reset_state):
"""環境變數完全未設 → 預設 false → 走 legacy"""
def test_flag_unset_defaults_to_ollama_first(self, monkeypatch, reset_state):
"""環境變數完全未設 → 預設 true → Ollama"""
monkeypatch.delenv('OPENCLAW_QA_OLLAMA_FIRST', raising=False)
legacy_called = {'count': 0}
ollama_called = {'count': 0}
def fake_legacy(q, ctx, request_id=None):
legacy_called['count'] += 1
return "[legacy reply]"
def fake_ollama(q, ctx, rid):
ollama_called['count'] += 1
return (
"Ollama 主路徑已接手競品分析。建議先檢查近七日價差、銷售跌幅、"
"PChome 優勢品項與高毛利 SKU再依 HIGH/MED/LOW 分層處理。"
"若價差超過 15% 且銷售下滑超過 20%,應優先送人工覆核與 Telegram 告警。"
)
monkeypatch.setattr(svc, '_legacy_gemini_first_qa', fake_legacy)
# 不 stub _call_qwen3_qa如果意外被呼叫會打到真網路 → fail
monkeypatch.setattr(svc, '_call_qwen3_qa', fake_ollama)
result = svc.generate_strategy_response("競品分析")
assert legacy_called['count'] == 1
assert result == "[legacy reply]"
assert ollama_called['count'] == 1
assert legacy_called['count'] == 0
assert result.startswith("Ollama 主路徑已接手")
def test_empty_query_short_circuits(self, monkeypatch, reset_state):
"""空 query 不應觸發任何 LLM 呼叫。"""