This commit is contained in:
@@ -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` 已覆蓋。
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')。
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 呼叫。"""
|
||||
|
||||
Reference in New Issue
Block a user