# services/event_router.py # # ADR-012 §③: 單一入口 dispatch(event) — L0/L1/L2 分流 # W2-C: L2 優先走 Elephant Alpha Orchestrator;EA 不可用時 fallback AIOrchestrator # import asyncio import logging from typing import Any, Dict, Optional from services.ai_orchestrator import AIOrchestrator from services.telegram_templates import alert from database.manager import get_session logger = logging.getLogger(__name__) async def _handle_l1(event: Dict[str, Any], session_id: str) -> Dict[str, Any]: """L1: semantic translation + reason analysis (Hermes).""" orchestrator = AIOrchestrator() return await orchestrator.handle_l1(event, session_id) async def _handle_l2(event: Dict[str, Any], session_id: str) -> Dict[str, Any]: """ L2: W2-C — EA Orchestrator 優先(動態路由 256K ctx); EA 不可用(API key 未設或連線失敗)時 fallback AIOrchestrator。 ADR-012: audit trail 由 EA._log_decision + triaged_alert 雙寫保證。 """ try: from services.elephant_service import elephant_service from services.elephant_alpha_orchestrator import elephant_orchestrator # 護欄:EA API key 未設定則直接 fallback,不嘗試連線 if not elephant_service.api_key: raise RuntimeError("OPENROUTER_API_KEY not configured, using fallback") # 護欄:連線快取確認(W3-A cache 300s,不會每次都 ping) if not elephant_service.check_connection(): raise RuntimeError("EA connection unavailable, using fallback") decision = await elephant_orchestrator.analyze_and_coordinate({ "event": event, "tier": "L2", "session_id": session_id, "urgency": "high", "complexity": "medium", "task_type": event.get("event_type", "general_analysis"), }) return { "source": "elephant_alpha", "priority": decision.priority, "confidence": decision.confidence, "execution_plan": decision.execution_plan, "agents_required": decision.agents_required, "reasoning": decision.reasoning, } except Exception as e: logger.warning(f"[EventRouter] EA L2 failed ({e}), fallback → AIOrchestrator") orchestrator = AIOrchestrator() return await orchestrator.handle_l2(event, session_id) async def _handle_l0(event: Dict[str, Any]) -> Dict[str, Any]: """L0: return raw event (compatibility/monitoring).""" return {"status": "ok", "echo": event.get("event_type")} async def dispatch(event: Dict[str, Any], admin_chat_ids: Optional[list] = None) -> Dict[str, Any]: """ Main event routing entry (ADR-012 §③ — 唯一入口). Output format compatible with routes/bot_api_routes. """ tier = _classify(event) session_id = f"evt:{event.get('event_type')}:{event.get('source', 'unknown')}" try: if tier == "L0": result = await _handle_l0(event) elif tier == "L1": result = await _handle_l1(event, session_id) elif tier == "L2": result = await _handle_l2(event, session_id) else: result = await _handle_l0(event) return { "tier": tier, "sent": 1, "errors": [], "latency_ms": 0, "payload": result, } except Exception as e: logger.exception(f"[EventRouter] dispatch failed: {e}") return { "tier": tier, "sent": 0, "errors": [str(e)], "latency_ms": 0, "payload": None, } def _classify(event: Dict[str, Any]) -> str: sev = event.get("severity", "info") has_trace = bool(event.get("trace")) event_type = event.get("event_type", "") if sev in ("info", "success"): return "L0" if sev == "warning": return "L1" if has_trace else "L0" if sev == "alert": if event_type in {"price_threat", "db_connection_error", "crawler_timeout", "nim_quota_exhausted", "embedding_failure"}: return "L2" return "L1" return "L0"