#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ services/llm_caller_registry.py Operation Ollama-First v5.0 / Phase 16 — caller 名稱集中註冊(critic-A11 L4 修補) 問題:caller 命名分散在 ai_call_logger 註解 / migrations/024 SQL 註解 / 各 service hardcode,三層不一致時報表會看到鬼影(gemini / Gemini / gemini-flash)。 修補:本檔為 single source of truth,ai_call_logger 啟動時驗證; service 寫死 caller 字串若不在 registry → log warning(不 raise,保留擴展彈性)。 依 ADR-028 caller 白名單(30+ 個 caller)。 """ from __future__ import annotations import logging logger = logging.getLogger(__name__) # ───────────────────────────────────────────────────────────────────────────── # Caller 白名單(按服務分組,與 ai_call_logger 註解 + migration 024 對齊) # ───────────────────────────────────────────────────────────────────────────── CALLER_REGISTRY: frozenset = frozenset({ # Hermes 競價分析(hermes_analyst_service) 'hermes_analyst', # _call_hermes_batch 'hermes_intent', # intent_classify (L1 NLP) 'hermes_ea_prefetch', # EA AI 例外決策 pre-fetch (ADR-021) # KM Embedding(openclaw_learning_service) 'km_embedding_worker', # 60s retry queue worker 'km_embedding_realtime', # _build_semantic_rag_context # AiderHeal(aider_heal_executor) 'aider_heal', # SSH CLI 跑 Aider(暫不接 logger) # MCP Collector(mcp_collector_service) 'mcp_l1_grounding', # Gemini 2.0 Flash Grounding 'mcp_l2_grounding', # Gemini 1.5 Flash Grounding 'mcp_l3_ollama', # Ollama 知識庫兜底 'mcp_collector', # Phase 10.5 omnisearch 入口 # 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 字 Ollama-first 洞察 'openclaw_daily_insight_gemini_fallback', 'openclaw_daily_insight_nim', 'openclaw_strategist', # Phase 10.5 mcp_router caller # NemoTron 派遣(nemoton_dispatcher_service) 'nemotron_dispatch', # NIM 8B 主路徑 / qwen3 主路徑 # Code Review(code_review_pipeline_service) 'code_review_hermes', # Step 2 Hermes 掃描 'code_review_openclaw', # Step 3 OpenClaw 評估(Ollama-first;Claude optional fallback) 'code_review_elephant', # Step 4 ElephantAlpha 49B 'code_review_openclaw_gemini', # Ollama/Claude 失敗後的 Gemini fallback # ElephantAlpha(elephant_alpha_*) 'ea_engine', # _execute_autonomous_decision (Gemini orchestrate) # PPT 簡報(routes/openclaw_bot_routes) 'ppt_gemini', # Gemini Flash 主分析 'ppt_ollama', # Ollama 失敗 fallback 'ppt_nim', # NIM deepseek-v3.2 主分析 'ppt_vision', # Phase 14 PPT 視覺檢查(qwen2-vl) # Sales / Trend(routes/ai_routes + routes/trend_routes) 'sales_copy', # 文案生成 'trend_match', # 商品比對 'trend_qa', # Web Search Q&A 'product_insights', # 商品洞察 'trend_keywords', # 趨勢關鍵字 # Telegram Bot 'tg_bot_copy', # /copy 文案 'tg_bot_copy_v2', # second copy entrance 'openclaw_bot_main', # OpenClaw Bot 主鏈 Ollama 'openclaw_bot_gemini', # Bot Gemini fallback 'openclaw_bot_image', # Bot 圖片商品辨識 Ollama-first 'openclaw_bot_image_gemini', # Bot 圖片商品辨識 Gemini fallback 'openclaw_bot_nim', # Bot NIM fallback # 其他 'bot_api_copy', # bot_api_routes 'trend_crawler', # trend_crawler_service 'ai_provider_generic', # ai_provider 抽象層 }) def is_known_caller(caller: str) -> bool: """檢查 caller 是否在白名單""" return caller in CALLER_REGISTRY def assert_known_caller(caller: str, strict: bool = False) -> None: """ai_call_logger 啟動時或寫入時驗證。 Args: caller: 待驗證的 caller 名 strict: True → 不在 registry 時 raise;False(預設)→ 只 log warning 依 ADR-028:新增 caller 必須先入 ADR + registry,再上 commit。 """ if not is_known_caller(caller): msg = f"unknown caller: {caller!r} not in CALLER_REGISTRY (ADR-028)" if strict: raise ValueError(msg) logger.warning(f"[CallerRegistry] {msg} — see services/llm_caller_registry.py") def list_callers_by_service() -> dict: """除錯/文件用:分組列出所有合法 caller""" return { 'hermes': [c for c in CALLER_REGISTRY if c.startswith('hermes_')], 'openclaw': [c for c in CALLER_REGISTRY if c.startswith('openclaw_') and not c.startswith('openclaw_bot_')], 'openclaw_bot': [c for c in CALLER_REGISTRY if c.startswith('openclaw_bot_')], 'mcp': [c for c in CALLER_REGISTRY if c.startswith('mcp_')], 'code_review': [c for c in CALLER_REGISTRY if c.startswith('code_review_')], 'ppt': [c for c in CALLER_REGISTRY if c.startswith('ppt_')], 'tg_bot': [c for c in CALLER_REGISTRY if c.startswith('tg_bot_')], 'km_embedding': [c for c in CALLER_REGISTRY if c.startswith('km_embedding_')], 'sales_trend': ['sales_copy', 'trend_match', 'trend_qa', 'product_insights', 'trend_keywords'], 'misc': ['ea_engine', 'aider_heal', 'nemotron_dispatch', 'bot_api_copy', 'trend_crawler', 'ai_provider_generic'], } __all__ = [ 'CALLER_REGISTRY', 'is_known_caller', 'assert_known_caller', 'list_callers_by_service', ]