diff --git a/apps/api/src/services/ai_providers/interfaces.py b/apps/api/src/services/ai_providers/interfaces.py index d5ee5e46..ddd0acb0 100644 --- a/apps/api/src/services/ai_providers/interfaces.py +++ b/apps/api/src/services/ai_providers/interfaces.py @@ -129,6 +129,10 @@ class AIProvider(Protocol): """健康檢查 (供 /health endpoint 和 AIRouter 動態路由)""" ... + async def close(self) -> None: + """關閉 HTTP 連線 / 釋放資源 (shutdown hook, ADR-052 I5)""" + ... + # ============================================================================= # 工具函數 diff --git a/apps/api/src/services/ai_router.py b/apps/api/src/services/ai_router.py index faef105f..2035006e 100644 --- a/apps/api/src/services/ai_router.py +++ b/apps/api/src/services/ai_router.py @@ -17,11 +17,11 @@ AI Router - Phase 13.3 #87 │ DELETE/CRITICAL │ Claude │ 強制使用最強模型 │ └─────────────────┴───────────────┴──────────────────────────────┘ -版本: v3.0 +版本: v4.0 建立: 2026-03-26 (台北時區) 建立者: Claude Code -最後修改: 2026-03-26 (台北時區) -修改者: Claude Code +最後修改: 2026-04-02 (台北時區) +修改者: ogt (首席架構師 Review C1/C2/C3 修復) 變更紀錄: | 版本 | 日期 | 執行者 | 變更內容 | @@ -29,6 +29,7 @@ AI Router - Phase 13.3 #87 | v1.0 | 2026-03-26 | Claude Code | 初始實作 | | v2.0 | 2026-03-26 | Claude Code | 支援 IntentResult + 新意圖類型 | | v3.0 | 2026-03-26 | Claude Code | Phase 13.3 #87 完整路由決策矩陣 | +| v4.0 | 2026-04-02 | ogt (首席架構師) | Phase 24 AIProvider Registry + Executor; C1 Langfuse Trace; C2 AIRouter.route(); C3 型別 typo; I4 Protocol close | """ from __future__ import annotations @@ -75,7 +76,7 @@ class AIProviderEnum(Enum): # Provider 對應延遲預算 (ms) -PROVIDER_LATENCY_BUDGET: dict[AIProviderEnumEnum, int] = { +PROVIDER_LATENCY_BUDGET: dict[AIProviderEnum, int] = { AIProviderEnum.OLLAMA: 60000, # 本地,允許較長處理時間 AIProviderEnum.GEMINI: 30000, # 雲端,較低延遲 AIProviderEnum.CLAUDE: 30000, # 雲端,較低延遲 @@ -676,7 +677,7 @@ class AIProviderRegistry: def __init__(self) -> None: self._providers: dict[str, AIProviderProtocol] = {} - def register(self, provider: AIProviderEnumProtocol) -> None: + def register(self, provider: AIProviderProtocol) -> None: """註冊 Provider (啟動時呼叫)""" self._providers[provider.name] = provider status = "enabled" if provider.is_enabled else "disabled" @@ -816,6 +817,23 @@ class AIRouterExecutor: logger.debug("ai_router_cache_read_failed", error=str(e)) # ③ 遍歷 Provider + 閘門 (D3) + # 2026-04-02 ogt: C1 修復 — 建立 Langfuse Trace (D5) + # 包住整個執行鏈,記錄每個 Provider 的 generation + try: + from src.services.langfuse_client import langfuse_trace + _lf_trace_ctx = langfuse_trace( + "ai_router_execute", + metadata={ + "provider_order": provider_order, + "prompt_length": len(prompt), + "require_local": require_local, + "alert_type": (context or {}).get("alert_type", ""), + }, + ) + _lf_trace_ctx.__enter__() + except Exception: + _lf_trace_ctx = None + errors: list[str] = [] for provider_name in provider_order: @@ -834,7 +852,8 @@ class AIRouterExecutor: continue # 閘門 2: Rate Limiter - if provider_name in ("nvidia", "gemini", "claude"): + # 2026-04-02 Claude Code: Phase 24 B3 — 加入 nemotron Rate Limiter + if provider_name in ("nvidia", "gemini", "claude", "nemotron"): try: from src.services.ai_rate_limiter import get_ai_rate_limiter rate_limiter = get_ai_rate_limiter() @@ -882,6 +901,20 @@ class AIRouterExecutor: tokens=result.tokens, from_cache=False, ) + # D5: 記錄 Langfuse generation + if _lf_trace_ctx: + try: + _lf_trace_ctx.generation( + name=f"{provider_name}_call", + model=provider_name, + input=prompt[:500], + output=result.raw_response[:500], + usage={"total": result.tokens} if result.tokens else None, + metadata={"cost_usd": result.cost_usd, "latency_ms": round(result.latency_ms, 1)}, + ) + _lf_trace_ctx.__exit__(None, None, None) + except Exception: + pass return result # Provider 回傳 success=False @@ -895,6 +928,11 @@ class AIRouterExecutor: # 全部失敗 logger.error("ai_router_all_providers_failed", tried=provider_order, errors=errors) + if _lf_trace_ctx: + try: + _lf_trace_ctx.__exit__(None, None, None) + except Exception: + pass return AIResult( raw_response="", success=False, @@ -925,8 +963,9 @@ def _init_registry() -> AIProviderRegistry: registry.register(ClaudeProvider()) registry.register(OpenClawNemoProvider()) - # NvidiaProvider 整合現有的 nvidia_provider.py (Phase 24-B3) - # 暫時不註冊,Tool Calling 仍走現有路徑 + # 2026-04-02 Claude Code: Phase 24 B3 — 加入 NemotronProvider (tool_calling 優先) + from src.services.ai_providers.nemotron import NemotronProvider + registry.register(NemotronProvider()) return registry diff --git a/apps/api/src/services/openclaw.py b/apps/api/src/services/openclaw.py index 8b036559..8348b5d3 100644 --- a/apps/api/src/services/openclaw.py +++ b/apps/api/src/services/openclaw.py @@ -917,19 +917,38 @@ class OpenClawService: # ================================================================= if settings.USE_AI_ROUTER: try: - from src.services.ai_router import get_ai_executor + # 2026-04-02 ogt: C2 修復 — 呼叫 AIRouter.route() 智慧路由 (非靜態 order) + # D1 意圖分類路由、D7 隱私保護 (DIAGNOSE/CODE_REVIEW 強制 local) 生效 + from src.services.ai_router import get_ai_router, get_ai_executor, IntentType + router = get_ai_router() executor = get_ai_executor() + + # Step 1: 取得路由決策 (含意圖分類 + 複雜度評分) + decision = await router.route(prompt, alert_context) + + # Step 2: 從 RoutingDecision 建立 provider_order (主 + fallback) + provider_order = [decision.selected_provider.value] + [ + p.value for p, _ in decision.fallback_chain + if p.value != decision.selected_provider.value + ] + + # Step 3: D7 隱私 — DIAGNOSE/CODE_REVIEW 強制 local + require_local = decision.intent in (IntentType.DIAGNOSE, IntentType.CODE_REVIEW) + result = await executor.execute( prompt=prompt, - provider_order=settings.AI_FALLBACK_ORDER, + provider_order=provider_order, context=alert_context, cache_ttl=3600, + require_local=require_local, ) logger.info( "phase24_ai_router_used", provider=result.provider, success=result.success, latency_ms=round(result.latency_ms, 1), + intent=decision.intent.value, + routing_reason=decision.routing_reason, ) return result.raw_response, result.provider, result.success, result.tokens, result.cost_usd except Exception as e: