From 875810620faa3087ae3f4c8aa3db903677979387 Mon Sep 17 00:00:00 2001 From: OoO Date: Wed, 20 May 2026 13:00:39 +0800 Subject: [PATCH] =?UTF-8?q?[V10.327]=20=E8=A3=9C=20OpenClaw=20=E5=82=99?= =?UTF-8?q?=E6=8F=B4=E5=8F=AF=E8=A7=80=E6=B8=AC=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO_NEXT_STEPS.txt | 1 + config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 3 +- routes/admin_observability_routes.py | 13 ++++++-- routes/openclaw_bot_routes.py | 2 +- services/llm_caller_registry.py | 12 +++++++- services/mcp_collector_service.py | 6 ++-- tests/test_caller_registry.py | 6 ++++ tests/test_mcp_collector_ollama_first.py | 38 ++++++++++++++++++++++++ 9 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 tests/test_mcp_collector_ollama_first.py diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 319b704..ad020c0 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.327 補 OpenClaw fallback 可觀測性:週報、月報、Meta、日報洞察、每日報告的 Gemini/NIM 備援 caller 納入 caller registry、AI 觀測台 agent group 與 Telegram 狀態統計,並補 MCP collector Ollama-first regression test,避免 fallback 真實使用量在觀測層被歸類成未知或漏算。 - V10.326 補市場情報 candidate queue review AI summary Telegram dispatch report run readiness:新增 read-only report run readiness builder、POST endpoint、UI 按鈕與 deployment readiness smoke target,在 report run package 後整理 report generation readiness manifest、manual report command boundary、artifact path gate 與後續 report run receipt gate;API/UI 不讀 approval/Telegram token、不呼叫 LLM、不派送 Telegram、不開 DB、不寫檔、不產報表、不更新 review_state、不掛 scheduler。 - V10.325 收斂 Gemini 主路徑:OpenClaw 週/月/meta/日報洞察、Telegram PPT 分析與 MCP fallback 全部改成先走 OllamaService 的 GCP-A → GCP-B → 111 三主機級聯;Gemini 只在 Ollama/NIM 不可用後作備援。Elephant Alpha resource_optimization 告警補上待處理 action_plans 焦點列表,避免只報隊列數字卻沒有可執行對象。 - V10.324 補市場情報 candidate queue review AI summary Telegram dispatch report run package:新增 read-only report run package builder、POST endpoint、UI 按鈕與 deployment readiness smoke target,在 report input 後整理 run package contract、evidence refs、package sections 與後續 report run readiness gate;API/UI 不讀 approval/Telegram token、不呼叫 LLM、不派送 Telegram、不開 DB、不寫檔、不產報表、不更新 review_state、不掛 scheduler。 diff --git a/config.py b/config.py index b26fdd0..b83ef61 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.326" +SYSTEM_VERSION = "V10.327" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index c46fdd4..a2928be 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -2,7 +2,7 @@ > **最後更新**: 2026-05-20 (台北時間) > **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 僅備援 -> **適用版本**: V10.325 +> **適用版本**: V10.327 --- @@ -23,6 +23,7 @@ - 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 → 111;Gemini 只允許以 `openclaw_bot_image_gemini` caller 作為失敗後備援。 - OpenClaw 週報、月報、Meta analysis、日報洞察、Telegram PPT 分析與 MCP fallback 也必須 Ollama-first;Gemini caller 只能帶 `_gemini_fallback` 或明確 fallback caller 語意,且不得先於 Ollama/NIM 被呼叫。 +- OpenClaw 週報、月報、Meta analysis、日報洞察與每日報告的 Gemini/NIM 備援 caller 必須登錄在 caller registry、AI 觀測台 agent group 與 Telegram 狀態統計,避免 fallback 用量被歸類為未知或漏算。 ## 一、四 AI Agent 路由架構 diff --git a/routes/admin_observability_routes.py b/routes/admin_observability_routes.py index 8e418d6..730b7a6 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -46,6 +46,11 @@ _GEMINI_BACKUP_CALLER_DISPLAY = { } _GEMINI_BACKUP_CALLERS = { 'code_review_openclaw_gemini', + 'openclaw_daily_gemini_fallback', + 'openclaw_daily_insight_gemini_fallback', + 'openclaw_weekly_gemini_fallback', + 'openclaw_monthly_gemini_fallback', + 'openclaw_meta_gemini_fallback', 'openclaw_qa_gemini_fallback', 'openclaw_bot_gemini', 'openclaw_bot_image_gemini', @@ -763,8 +768,12 @@ def business_intel_dashboard(): _AGENT_CALLER_GROUPS = { 'openclaw': [ 'openclaw_qa', 'openclaw_qa_gemini_fallback', 'openclaw_qa_nim', - 'openclaw_daily', 'openclaw_daily_insight', - 'openclaw_meta', 'openclaw_monthly', 'openclaw_weekly', + 'openclaw_daily', 'openclaw_daily_gemini_fallback', 'openclaw_daily_nim', + 'openclaw_daily_insight', 'openclaw_daily_insight_gemini_fallback', + 'openclaw_daily_insight_nim', + 'openclaw_meta', 'openclaw_meta_gemini_fallback', 'openclaw_meta_nim', + 'openclaw_monthly', 'openclaw_monthly_gemini_fallback', 'openclaw_monthly_nim', + 'openclaw_weekly', 'openclaw_weekly_gemini_fallback', 'openclaw_weekly_nim', 'openclaw_bot_main', 'openclaw_bot_gemini', 'openclaw_bot_nim', 'sales_copy', 'code_review_openclaw', 'code_review_openclaw_gemini', ], diff --git a/routes/openclaw_bot_routes.py b/routes/openclaw_bot_routes.py index dbbc4c3..ebbf676 100644 --- a/routes/openclaw_bot_routes.py +++ b/routes/openclaw_bot_routes.py @@ -8482,7 +8482,7 @@ def handle_cmd(cmd, arg, chat_id, reply_to): from database.manager import DatabaseManager from sqlalchemy import text as _sa agent_groups = [ - ('🤖 OpenClaw', ['openclaw_qa', 'openclaw_qa_gemini_fallback', 'openclaw_qa_nim', 'openclaw_daily', 'openclaw_meta', 'openclaw_monthly', 'openclaw_weekly', 'openclaw_bot_main', 'openclaw_bot_gemini', 'openclaw_bot_nim', 'sales_copy', 'code_review_openclaw', 'code_review_openclaw_gemini', 'openclaw_daily_insight']), + ('🤖 OpenClaw', ['openclaw_qa', 'openclaw_qa_gemini_fallback', 'openclaw_qa_nim', 'openclaw_daily', 'openclaw_daily_gemini_fallback', 'openclaw_daily_nim', 'openclaw_meta', 'openclaw_meta_gemini_fallback', 'openclaw_meta_nim', 'openclaw_monthly', 'openclaw_monthly_gemini_fallback', 'openclaw_monthly_nim', 'openclaw_weekly', 'openclaw_weekly_gemini_fallback', 'openclaw_weekly_nim', 'openclaw_bot_main', 'openclaw_bot_gemini', 'openclaw_bot_nim', 'sales_copy', 'code_review_openclaw', 'code_review_openclaw_gemini', 'openclaw_daily_insight', 'openclaw_daily_insight_gemini_fallback', 'openclaw_daily_insight_nim']), ('🔍 Hermes', ['hermes_analyst', 'hermes_intent', 'code_review_hermes']), ('🧬 NemoTron', ['nemotron_dispatch']), ('🐘 ElephantAlpha', ['ea_engine', 'code_review_elephant']), diff --git a/services/llm_caller_registry.py b/services/llm_caller_registry.py index 876f55e..8c17721 100644 --- a/services/llm_caller_registry.py +++ b/services/llm_caller_registry.py @@ -43,13 +43,23 @@ CALLER_REGISTRY: frozenset = frozenset({ # OpenClaw 戰略(openclaw_strategist_service) 'openclaw_daily', # 每日報告 + 'openclaw_daily_gemini_fallback', + 'openclaw_daily_nim', 'openclaw_weekly', # 週一 06:00 + 'openclaw_weekly_gemini_fallback', + 'openclaw_weekly_nim', 'openclaw_monthly', # 每月 1 日 + 'openclaw_monthly_gemini_fallback', + 'openclaw_monthly_nim', 'openclaw_meta', # Meta 自審 12:00 + 'openclaw_meta_gemini_fallback', + 'openclaw_meta_nim', 'openclaw_qa', # Telegram Q&A 'openclaw_qa_gemini_fallback', # Telegram Q&A Gemini fallback 'openclaw_qa_nim', # Telegram Q&A NIM fallback - 'openclaw_daily_insight', # Phase 3 A8 拆分後的 200 字 Gemini 洞察 + 'openclaw_daily_insight', # Phase 3 A8 拆分後的 200 字 Ollama-first 洞察 + 'openclaw_daily_insight_gemini_fallback', + 'openclaw_daily_insight_nim', 'openclaw_strategist', # Phase 10.5 mcp_router caller # NemoTron 派遣(nemoton_dispatcher_service) diff --git a/services/mcp_collector_service.py b/services/mcp_collector_service.py index f815859..e510620 100644 --- a/services/mcp_collector_service.py +++ b/services/mcp_collector_service.py @@ -4,7 +4,7 @@ services/mcp_collector_service.py MCP 外部情報收集層 -透過 Gemini Google Search Grounding 收集外部市場情報,供 OpenClaw 戰略分析使用: +透過 MCP omnisearch / Ollama-first fallback / Gemini final fallback 收集外部市場情報,供 OpenClaw 戰略分析使用: - 台灣電商市場趨勢 - 節日 / 促銷行事曆 - 季節性消費洞察 @@ -29,8 +29,8 @@ logger = logging.getLogger(__name__) GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "") MCP_CACHE_TTL_HOURS = int(os.getenv("MCP_CACHE_TTL_HOURS", "24")) -# LOCKED-GEMINI: MCP 即時情報需 Google Search Grounding,唯一聯網能力(ADR-028 鎖定場景 #1) -# Ollama 為離線模型,知識截止於訓練日,不可取代 grounding。 +# MCP router 是即時情報主路徑;router 不可用時先走 Ollama 三主機級聯做離線洞察, +# Gemini Grounding 僅作最後備援,避免再次回到 Gemini-first。 MCP_MODEL = os.getenv("MCP_GEMINI_MODEL", "gemini-2.0-flash") MCP_FALLBACK_MODEL = "gemini-1.5-flash" diff --git a/tests/test_caller_registry.py b/tests/test_caller_registry.py index 5817de3..eceafdb 100644 --- a/tests/test_caller_registry.py +++ b/tests/test_caller_registry.py @@ -28,6 +28,12 @@ def test_registry_contains_core_callers(): 'openclaw_daily', 'openclaw_weekly', 'openclaw_monthly', 'openclaw_meta', 'openclaw_qa', 'openclaw_qa_gemini_fallback', 'openclaw_qa_nim', + 'openclaw_daily_gemini_fallback', 'openclaw_weekly_gemini_fallback', + 'openclaw_monthly_gemini_fallback', 'openclaw_meta_gemini_fallback', + 'openclaw_daily_insight_gemini_fallback', + 'openclaw_daily_nim', 'openclaw_weekly_nim', + 'openclaw_monthly_nim', 'openclaw_meta_nim', + 'openclaw_daily_insight_nim', # MCP 'mcp_l1_grounding', 'mcp_collector', # Code Review diff --git a/tests/test_mcp_collector_ollama_first.py b/tests/test_mcp_collector_ollama_first.py new file mode 100644 index 0000000..3693e1a --- /dev/null +++ b/tests/test_mcp_collector_ollama_first.py @@ -0,0 +1,38 @@ +from types import SimpleNamespace + + +def test_mcp_collector_uses_ollama_before_gemini(monkeypatch): + import services.mcp_collector_service as mcp_mod + import services.mcp_router as router_mod + + service = mcp_mod.MCPCollectorService() + monkeypatch.setattr(service, "_read_cache", lambda _topic: None) + monkeypatch.setattr(router_mod, "is_mcp_router_enabled", lambda: False) + monkeypatch.setattr(mcp_mod, "_OLLAMA_AVAILABLE", True) + + def fail_gemini_init(): + raise AssertionError("Gemini must not initialize before Ollama fallback is tried") + + calls = [] + + class FakeOllamaService: + def __init__(self, model=None): + self.model = model + + def generate(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace( + success=True, + content="(此為基於歷史趨勢的預測性洞察) 台灣電商促銷可先檢查母親節、618 與競品價差。", + error=None, + ) + + monkeypatch.setattr(service, "_ensure_init", fail_gemini_init) + monkeypatch.setattr(mcp_mod, "OllamaService", FakeOllamaService) + + result = service._search_topic("market_trends", "台灣電商促銷趨勢") + + assert "基於歷史趨勢" in result + assert calls + assert calls[0]["model"] + assert calls[0]["options"]["num_predict"] == 800