V10.538 對齊 ai_calls ollama_other provider
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
This commit is contained in:
@@ -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`。
|
||||
|
||||
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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`。
|
||||
|
||||
@@ -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,不再用全表最新價子查詢。
|
||||
|
||||
32
migrations/043_allow_ollama_other_ai_calls_provider.sql
Normal file
32
migrations/043_allow_ollama_other_ai_calls_provider.sql
Normal file
@@ -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;
|
||||
@@ -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),
|
||||
|
||||
@@ -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 測試
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user