diff --git a/docs/memory/claude_inventory_validation_20260513.md b/docs/memory/claude_inventory_validation_20260513.md index bbb400c..e8657d2 100644 --- a/docs/memory/claude_inventory_validation_20260513.md +++ b/docs/memory/claude_inventory_validation_20260513.md @@ -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:` 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` 已覆蓋。 diff --git a/services/nemoton_dispatcher_service.py b/services/nemoton_dispatcher_service.py index 59e9b61..dc9b016 100644 --- a/services/nemoton_dispatcher_service.py +++ b/services/nemoton_dispatcher_service.py @@ -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:14b(Ollama 官方 + qwenlm 雙確認 tools 支援)。 @@ -1397,7 +1397,7 @@ class NemotronDispatcher: } # ── Operation Ollama-First v5.0 / Phase 3 / A9:qwen3 主路徑(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) diff --git a/services/openclaw_strategist_service.py b/services/openclaw_strategist_service.py index f76a669..a295070 100644 --- a/services/openclaw_strategist_service.py +++ b/services/openclaw_strategist_service.py @@ -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(預設 ON;Gemini 僅 fallback) +# - OPENCLAW_QA_OLLAMA_FIRST: true=走 Ollama 主、Gemini fallback;false=緊急退回 legacy Gemini-first # - OPENCLAW_QA_OLLAMA_MODEL: GCP Ollama 上的模型 tag(A2 推薦 qwen3:14b,9.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-first(feature 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 → NIM(A4 已接 ai_call_logger)── + # ── 備援路徑:Gemini → NIM(A4 已接 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 指令:「免費優先」 # 預設 ON:Hermes 算 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')。 diff --git a/tests/test_nemotron_qwen3_compat.py b/tests/test_nemotron_qwen3_compat.py index 9c7dad3..742fb79 100644 --- a/tests/test_nemotron_qwen3_compat.py +++ b/tests/test_nemotron_qwen3_compat.py @@ -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 diff --git a/tests/test_openclaw_daily_template.py b/tests/test_openclaw_daily_template.py index abbca2a..404fca4 100644 --- a/tests/test_openclaw_daily_template.py +++ b/tests/test_openclaw_daily_template.py @@ -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_report(regression) - 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 設定時 → 預設 true(Hermes/Ollama-first)""" # 不 set env import importlib import services.openclaw_strategist_service as svc diff --git a/tests/test_openclaw_qa_routing.py b/tests/test_openclaw_qa_routing.py index 70368cd..93bed4b 100644 --- a/tests/test_openclaw_qa_routing.py +++ b/tests/test_openclaw_qa_routing.py @@ -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-first(regression 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=true(Ollama-first) monkeypatch.delenv('OPENCLAW_QA_OLLAMA_FIRST', raising=False) yield captured @@ -136,13 +136,13 @@ class TestLowQualityRules: # ───────────────────────────────────────────────────────────────────────────── -# 2. Routing:feature flag = false 時維持 Gemini-first 路徑(regression) +# 2. Routing:feature 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 呼叫。"""