From 12c8c7e94d2f273f6de9cdb059d8b57db7a91ffb Mon Sep 17 00:00:00 2001 From: OoO Date: Mon, 1 Jun 2026 02:37:12 +0800 Subject: [PATCH] =?UTF-8?q?V10.538=20=E5=B0=8D=E9=BD=8A=20ai=5Fcalls=20oll?= =?UTF-8?q?ama=5Fother=20provider?= 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 | 1 + docs/memory/history_logs.md | 1 + ...3_allow_ollama_other_ai_calls_provider.sql | 32 +++++++++++++ services/ai_call_logger.py | 45 +++++++++++++++++-- tests/test_ai_call_logger.py | 17 +++++++ tests/test_migration_metadata_coverage.py | 9 ++++ tests/test_ollama_host_label.py | 2 +- 9 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 migrations/043_allow_ollama_other_ai_calls_provider.sql diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 318fa3c..60456b8 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.538 修 ai_calls provider CHECK 對齊:Hermes/Ollama 全失敗或未選定 host 時的 `ollama_other` 只作 telemetry bucket,migration 043 放行此值,`ai_call_logger` 也會將空值/unknown/非白名單 provider 正規化,避免觀測寫入失敗。 - V10.537 將 V10.536 focused exact 線接進 `run_retryable_candidate_revalidation()` 窄門:既有 `true_low_confidence` 舊候選若命中新品線且無 hard veto / 型別、款式、香味、件數、組合阻擋,就可重新走 matcher 寫入正式價差;有色號/香味/即期等阻擋仍不進回刷。 - V10.536 補 PChome 高分 `true_low_confidence` 安全救回線:新增花美水 Relax 薰衣草潤滑凝膠 1.7g x3、St.Clare 私密呼呼慕斯 x2 / 慕斯+噴霧組、BIOPEUTIC 果酸煥膚水凝乳 20% 150ml、台塑生醫嬰兒沐浴洗髮 3 件組、Elizabeth Arden 八小時護唇膏 SPF15 3.7g x3、理膚寶水全面修復潤唇膏 7.5ml focused total-price 規則;這些都要求同品牌、同品線與同規格/同組合,仍保留色號、香味、款式敏感品的 `variant_selection_review` 防線。 - V10.535 修 ElephantAlpha 價格 trigger statement timeout:`price_drop_alert` / `market_opportunity` / DB evidence prefetch 改為先篩最近有效 PChome identity_v2,再用 `JOIN LATERAL` 查單一 SKU 最新 MOMO 價格;保留 match_score/tags/diagnostic evidence,避免 scheduler 週期性重查整張 `price_records`。 diff --git a/config.py b/config.py index 1d943e5..073241b 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.537" +SYSTEM_VERSION = "V10.538" 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 baa2853..52f5166 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -32,6 +32,7 @@ - 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 strategy 的 Ollama `keep_alive` 預設為 `5m`,避免報告型任務把 GCP-B/111 runner 長駐 24h。 - OpenClaw 週報、月報、Meta analysis、日報洞察與每日報告的 Gemini/NIM 備援 caller 必須登錄在 caller registry、AI 觀測台 agent group 與 Telegram 狀態統計,避免 fallback 用量被歸類為未知或漏算。 +- `ai_calls.provider='ollama_other'` 只允許作為 unresolved/unknown Ollama telemetry bucket,例如全 host 失敗、尚未選定實際 GCP-A/GCP-B/111 host 或舊 caller 未帶 host;不得把 `ollama_other` 當成實際路由目標或新增非核准 Ollama host。 - Gemini API 出站有第二道 kill switch:`GEMINI_FALLBACK_ENABLED` 預設為 `false`。即使 `GEMINI_API_KEY` 存在,通用 AI fallback、OpenClaw 報告/QA/PPT/圖片、MCP Grounding 與 Code Review L3 都不得呼叫 Gemini;只有操作員明確設為 `true` 時,Gemini 才能作緊急備援。 - `docker-compose.yml` 的 `momo-app`、`scheduler`、`telegram-bot` 必須明確設定 `GEMINI_API_HARD_DISABLED=${GEMINI_API_HARD_DISABLED:-true}` 與 `GEMINI_FALLBACK_ENABLED=${GEMINI_FALLBACK_ENABLED:-false}`;`.env` 可保留 `GEMINI_API_KEY`,但不得因 key 存在就讓核心容器產生 Gemini 付費出站。 - Gemini 不可被任何狀態面板或 router 推薦為主提供者:`AIProviderService._get_recommended_provider()` 不得回傳 `gemini`,只能顯示為 fallback 狀態;`llm_model_router` 的 `ea_engine` 若收到 `gemini-*` default 必須改回 `hermes3:latest`,需要深推理時才升本地 `deepseek-r1:14b`。 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index 50897ef..4d3de3e 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-06-01:PChome 比價新鮮度操作閉環 +- **V10.538 ai_calls provider CHECK 對齊**: 正式 scheduler Hermes/Ollama 全失敗時會以 `provider=ollama_other` 記錄未知/未選定 host,但 `ai_calls.chk_ai_calls_provider` 舊白名單未包含此 telemetry bucket,導致觀測寫入再撞 CHECK。新增 migration 043 放行 `ollama_other`,並讓 `ai_call_logger` 將空值、unknown、非白名單 provider 字串正規化為允許值;`ollama_other` 僅作遙測分類,不是模型路由目標。 - **V10.537 focused exact revalidation queue 接線**: V10.536 只補 matcher 規則會影響新抓取,但無法批次回收既有 `true_low_confidence` 舊候選。`run_retryable_candidate_revalidation()` 新增 focused true-low 窄門:花美水 Relax、St.Clare 私密呼呼、BIOPEUTIC 果酸、台塑生醫、Elizabeth Arden 與理膚寶水若無 hard veto 與型別/款式/香味/件數/組合阻擋,可進重評;`variant_selection_review` 僅在這些具名安全線允許重跑,其他人工池不打開。 - **V10.536 高分 true_low_confidence focused exact 救回**: production 抽樣顯示剩餘未覆蓋集中在 `identity_veto` 與 `true_low_confidence`,其中部分 1.0 分樣本是同品牌、同品線、同規格/同組合,但因多件組或護唇/私密護理類型辨識保守而停在 manual review。新增花美水 Relax、St.Clare 私密呼呼、BIOPEUTIC 果酸、台塑生醫嬰兒沐浴洗髮、Elizabeth Arden 八小時護唇膏、理膚寶水全面修復潤唇膏的 focused total-price 規則;不放寬 `MIN_MATCH_SCORE`,也不移除色號/香味/款式防線。 - **V10.535 ElephantAlpha price trigger 查詢瘦身**: 正式 scheduler 日誌顯示 `price_drop_alert` trigger 對整張 `price_records` 做 `DISTINCT ON` 最新價造成 statement timeout。`price_drop_alert`、`market_opportunity` 與 EA DB evidence prefetch 改為先篩最近有效 PChome identity_v2 競品,再用 `JOIN LATERAL` 只查該 SKU 最新 MOMO 價格,保留 match_score/tags/diagnostic evidence 給 Telegram HITL,不再用全表最新價子查詢。 diff --git a/migrations/043_allow_ollama_other_ai_calls_provider.sql b/migrations/043_allow_ollama_other_ai_calls_provider.sql new file mode 100644 index 0000000..0e116a6 --- /dev/null +++ b/migrations/043_allow_ollama_other_ai_calls_provider.sql @@ -0,0 +1,32 @@ +-- Migration 043: allow telemetry-only ollama_other provider +-- Date: 2026-06-01 +-- +-- Runtime code maps unresolved or unknown Ollama hosts to `ollama_other`. +-- This is not a routing target; it is an observability bucket for failures +-- that happen before a concrete GCP-A/GCP-B/111 host is selected. + +BEGIN; + +ALTER TABLE IF EXISTS ai_calls + DROP CONSTRAINT IF EXISTS chk_ai_calls_provider; + +ALTER TABLE IF EXISTS ai_calls + ADD CONSTRAINT chk_ai_calls_provider + CHECK ( + provider IN ( + 'gcp_ollama', + 'ollama_secondary', + 'ollama_111', + 'ollama_other', + 'gemini', + 'claude', + 'nim', + 'openrouter', + 'nim_via_elephant' + ) + ) NOT VALID; + +COMMENT ON CONSTRAINT chk_ai_calls_provider ON ai_calls IS + 'Provider telemetry whitelist; ollama_other is unresolved/unknown Ollama host telemetry, not a route.'; + +COMMIT; diff --git a/services/ai_call_logger.py b/services/ai_call_logger.py index 3dff160..9e98a39 100644 --- a/services/ai_call_logger.py +++ b/services/ai_call_logger.py @@ -85,6 +85,45 @@ _MAX_CONSECUTIVE_FAILURES = 10 _failure_counter_lock = threading.Lock() _failure_state = {'count': 0, 'killed': False} +_AI_CALL_PROVIDER_WHITELIST = frozenset({ + 'gcp_ollama', + 'ollama_secondary', + 'ollama_111', + 'ollama_other', + 'gemini', + 'claude', + 'nim', + 'openrouter', + 'nim_via_elephant', +}) + + +def _normalize_provider(provider: str) -> str: + """Return an ai_calls.provider value that satisfies the DB CHECK. + + `ollama_other` is telemetry-only: it represents an approved Ollama route + whose concrete host was unknown or failed before a successful host was + selected. It must not be used as a routing target. + """ + text = (provider or '').strip()[:32] + if text in _AI_CALL_PROVIDER_WHITELIST: + return text + + lowered = text.lower() + if not lowered or lowered in {'unknown', 'none', 'null', 'ollama', 'ollama_local'}: + return 'ollama_other' + if lowered.startswith('gemini'): + return 'gemini' + if lowered.startswith(('claude', 'anthropic')): + return 'claude' + if lowered.startswith(('nim', 'nvidia', 'meta/')): + return 'nim' + if lowered.startswith('openrouter'): + return 'openrouter' + if 'ollama' in lowered: + return 'ollama_other' + return 'ollama_other' + def _record_failure() -> None: with _failure_counter_lock: @@ -145,7 +184,7 @@ class _CallState: ) self.caller = caller - self.provider = provider + self.provider = _normalize_provider(provider) self.model = model self.request_id = request_id self.input_tokens = 0 @@ -174,7 +213,7 @@ class _CallState: def set_provider(self, provider: str) -> None: """更新實際 provider。適用於 Ollama 三主機 retry 後才知道落點的 caller。""" if provider: - self.provider = provider[:32] + self.provider = _normalize_provider(provider) def set_model(self, model: str) -> None: """更新實際模型。適用於 host-aware downgrade 後才知道落點模型的 caller。""" @@ -387,7 +426,7 @@ def _write_to_db(state: _CallState) -> None: """), { 'caller': state.caller[:64] if state.caller else 'unknown', - 'provider': (state.provider or 'unknown')[:32], + 'provider': _normalize_provider(state.provider), 'model': (state.model or 'unknown')[:128], 'input_tokens': int(state.input_tokens or 0), 'output_tokens': int(state.output_tokens or 0), diff --git a/tests/test_ai_call_logger.py b/tests/test_ai_call_logger.py index 8a31cc8..67b67c6 100644 --- a/tests/test_ai_call_logger.py +++ b/tests/test_ai_call_logger.py @@ -32,6 +32,7 @@ from services.ai_call_logger import ( _calc_cost, _CallState, _is_logging_enabled, + _normalize_provider, _reset_kill_switch, log_ai_call, logged_ai_call, @@ -185,6 +186,22 @@ def test_context_manager_can_update_actual_provider_after_retry(reset_state): assert rec['provider'] == 'ollama_secondary' +def test_provider_normalization_keeps_ai_calls_check_safe(reset_state): + assert _normalize_provider('') == 'ollama_other' + assert _normalize_provider('unknown') == 'ollama_other' + assert _normalize_provider('ollama_other') == 'ollama_other' + assert _normalize_provider('gemini-2.5-flash') == 'gemini' + assert _normalize_provider('anthropic') == 'claude' + assert _normalize_provider('nvidia/nemotron') == 'nim' + + captured = reset_state + with log_ai_call('hermes_analyst', 'unknown', 'hermes3:latest'): + pass + + assert _wait_for_async(captured, 1) + assert captured[0]['provider'] == 'ollama_other' + + # ───────────────────────────────────────────────────────────────────────────── # decorator 測試 # ───────────────────────────────────────────────────────────────────────────── diff --git a/tests/test_migration_metadata_coverage.py b/tests/test_migration_metadata_coverage.py index 0a71f26..c9e2a95 100644 --- a/tests/test_migration_metadata_coverage.py +++ b/tests/test_migration_metadata_coverage.py @@ -65,6 +65,15 @@ def test_host_health_probe_label_check_accepts_runtime_labels(): assert f"'{label}'" in migration +def test_ai_calls_provider_check_accepts_ollama_other_telemetry_bucket(): + migration = (ROOT / "migrations" / "043_allow_ollama_other_ai_calls_provider.sql").read_text(encoding="utf-8") + + assert "DROP CONSTRAINT IF EXISTS chk_ai_calls_provider" in migration + assert "ADD CONSTRAINT chk_ai_calls_provider" in migration + assert "'ollama_other'" in migration + assert "NOT VALID" in migration + + def test_rag_embedding_signature_migration_covers_query_and_learning_tables(): migration = (ROOT / "migrations" / "034_add_embedding_signature_to_rag_tables.sql").read_text(encoding="utf-8") diff --git a/tests/test_ollama_host_label.py b/tests/test_ollama_host_label.py index e8c5917..2c7f01a 100644 --- a/tests/test_ollama_host_label.py +++ b/tests/test_ollama_host_label.py @@ -76,7 +76,7 @@ class TestGetProviderTag: assert get_provider_tag('http://192.168.0.188:11434') == 'ollama_other' @pytest.mark.parametrize('host,expected', [ - # 對齊 ai_calls 表 CHECK constraint 白名單 + # 對齊 ai_calls 表 CHECK constraint 白名單(migration 043 補 ollama_other) ('http://34.143.170.20:11434', 'gcp_ollama'), ('http://192.168.0.110:11435', 'gcp_ollama'), ('http://34.21.145.224:11434', 'ollama_secondary'),