diff --git a/apps/api/src/services/chat_manager.py b/apps/api/src/services/chat_manager.py index e779f64d..7ccd1b9b 100644 --- a/apps/api/src/services/chat_manager.py +++ b/apps/api/src/services/chat_manager.py @@ -5,13 +5,13 @@ Phase 21.5 初版: 2026-03-31 ogt Phase 22.6 重寫: 2026-04-03 ogt (統帥需求: 雙 AI 互動對話) 功能: -1. @openclaw / @nemo 路由 — 指定 AI 回應 -2. 無前綴 — 兩個 AI 輪流回應,並互相評論 -3. AI 互相對話 — NemoClaw 看到 OpenClaw 的回應後可補充/反駁 +1. @openclaw → 只有 OpenClaw 回應 +2. @nemo → 只有 NemoClaw 回應 +3. 無前綴 → OpenClaw 先答,NemoClaw 評論/反駁 -架構: -- OpenClaw: 用 Ollama qwen2.5:7b-instruct (本地, 快) -- NemoClaw: 用 Gemini Flash (雲端, 快) — NIM nemotron-mini 太慢 (15s+) +後端: +- 雙 AI 皆用 Gemini Flash,靠不同 persona 區分人格 +- Ollama 188 目前卡死 (0 bytes/30s),待主機重啟後可切換回來 """ import structlog @@ -21,7 +21,6 @@ from src.repositories.incident_repository import get_incident_repository logger = structlog.get_logger(__name__) -# 人格設定 OPENCLAW_PERSONA = """你是 OpenClaw,AWOOOI 平台的 SRE AI 主帥。 個性: 精準、果斷、專業,像老將一樣直接給出建議。 語氣: 簡短有力,不廢話。繁體中文回應。 @@ -65,43 +64,26 @@ class ChatManager: except Exception: incident_summary = "無法取得告警" - return f"""## 系統狀態 ({now.strftime('%Y-%m-%d %H:%M')} 台北) -- {cluster_info} -- 活躍告警: {incident_summary} -""" + return ( + f"## 系統狀態 ({now.strftime('%Y-%m-%d %H:%M')} 台北)\n" + f"- {cluster_info}\n" + f"- 活躍告警: {incident_summary}\n" + ) - async def _call_ollama(self, system_prompt: str, user_message: str) -> str: - """呼叫 Ollama (OpenClaw 用)""" - import httpx - try: - async with httpx.AsyncClient(timeout=30.0) as client: - resp = await client.post( - "http://192.168.0.188:11434/api/chat", - json={ - "model": "qwen2.5:7b-instruct", - "messages": [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_message}, - ], - "stream": False, - "options": {"temperature": 0.7, "num_predict": 512}, - }, - ) - resp.raise_for_status() - data = resp.json() - return data.get("message", {}).get("content", "").strip() - except Exception as e: - logger.warning("ollama_chat_failed", error=str(e)) - return None + async def _call_gemini(self, system_prompt: str, user_message: str, temperature: float = 0.7) -> str | None: + """ + 呼叫 Gemini Flash - async def _call_gemini(self, system_prompt: str, user_message: str) -> str: - """呼叫 Gemini Flash (NemoClaw 用)""" + 2026-04-03 ogt: 雙 AI 皆走 Gemini,用不同 persona 區分 + OpenClaw temperature=0.5 (精準), NemoClaw temperature=0.9 (發散) + """ import httpx from src.core.config import get_settings settings = get_settings() - api_key = settings.GEMINI_API_KEY if hasattr(settings, 'GEMINI_API_KEY') else None + api_key = getattr(settings, 'GEMINI_API_KEY', None) if not api_key: + logger.warning("gemini_api_key_not_set") return None try: @@ -111,7 +93,7 @@ class ChatManager: f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={api_key}", json={ "contents": [{"role": "user", "parts": [{"text": full_prompt}]}], - "generationConfig": {"temperature": 0.8, "maxOutputTokens": 512}, + "generationConfig": {"temperature": temperature, "maxOutputTokens": 512}, }, ) resp.raise_for_status() @@ -122,37 +104,27 @@ class ChatManager: return None async def _openclaw_respond(self, context: str, message: str) -> str: - """OpenClaw 回應""" - system = f"{OPENCLAW_PERSONA}\n{context}" - result = await self._call_ollama(system, message) + """OpenClaw 回應 (Gemini + OpenClaw persona, temperature=0.5)""" + result = await self._call_gemini(f"{OPENCLAW_PERSONA}\n{context}", message, temperature=0.5) if not result: - result = "🔴 OpenClaw 暫時離線,Ollama 無響應。" + result = "🔴 OpenClaw 暫時無法回應。" return f"🦞 OpenClaw:\n{result}" async def _nemoclaw_respond(self, context: str, message: str) -> str: - """NemoClaw 回應""" - system = f"{NEMOCLAW_PERSONA}\n{context}" - result = await self._call_gemini(system, message) + """NemoClaw 回應 (Gemini + NemoClaw persona, temperature=0.9)""" + result = await self._call_gemini(f"{NEMOCLAW_PERSONA}\n{context}", message, temperature=0.9) if not result: - # Gemini 失敗時 fallback 到 Ollama - result = await self._call_ollama(system, message) - if not result: - result = "🔴 NemoClaw 暫時離線。" + result = "🔴 NemoClaw 暫時無法回應。" return f"🤖 NemoClaw:\n{result}" - async def _nemoclaw_comment_on(self, context: str, openclaw_response: str, original_msg: str) -> str: + async def _nemoclaw_comment_on(self, context: str, openclaw_response: str, original_msg: str) -> str | None: """NemoClaw 評論 OpenClaw 的回應""" - message = f"""統帥問了: {original_msg} - -OpenClaw 的回應是: -{openclaw_response} - -請你從 NemoClaw 的角度評論上面的回應。可以補充、反駁、或提出不同觀點。""" - - system = f"{NEMOCLAW_PERSONA}\n{context}" - result = await self._call_gemini(system, message) - if not result: - result = await self._call_ollama(system, message) + message = ( + f"統帥問了: {original_msg}\n\n" + f"OpenClaw 剛才回應:\n{openclaw_response}\n\n" + f"請從 NemoClaw 角度評論。可以補充、反駁、或提出不同觀點。簡短有力。" + ) + result = await self._call_gemini(f"{NEMOCLAW_PERSONA}\n{context}", message, temperature=0.9) if not result: return None return f"🤖 NemoClaw 補充:\n{result}" @@ -168,7 +140,7 @@ OpenClaw 的回應是: @openclaw → 只有 OpenClaw 回應 @nemo → 只有 NemoClaw 回應 - 其他 → OpenClaw 先回,NemoClaw 評論 + 其他 → OpenClaw 先回,NemoClaw 補充/反駁 """ context = await self.get_system_context() text = message_text.strip() @@ -183,27 +155,24 @@ OpenClaw 的回應是: msg = text[5:].strip() or text return await self._nemoclaw_respond(context, msg) - # 模式 3: 雙 AI 對話 - # Step 1: OpenClaw 先回 - openclaw_raw = await self._call_ollama( - f"{OPENCLAW_PERSONA}\n{context}", text + # 模式 3: 雙 AI 對話 — OpenClaw 先,NemoClaw 評論 + openclaw_raw = await self._call_gemini( + f"{OPENCLAW_PERSONA}\n{context}", text, temperature=0.5 ) if not openclaw_raw: - openclaw_raw = "Ollama 無響應,OpenClaw 暫時離線。" + openclaw_raw = "Gemini 無響應,OpenClaw 暫時離線。" openclaw_block = f"🦞 OpenClaw:\n{openclaw_raw}" - # Step 2: NemoClaw 評論 OpenClaw 的回應 nemo_block = await self._nemoclaw_comment_on(context, openclaw_raw, text) if nemo_block: return f"{openclaw_block}\n\n{nemo_block}" - else: - return openclaw_block + return openclaw_block # Singleton -_chat_manager = None +_chat_manager: ChatManager | None = None def get_chat_manager() -> ChatManager: