From 82ce83b8b7c935bcec9a596bc0b826058a8d70a9 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Mon, 13 Apr 2026 13:21:02 +0900 Subject: [PATCH] =?UTF-8?q?feat(infra):=20Phase=202.1=20Gemma=204=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=9E=90=EC=97=B0=EC=96=B4=20=EC=84=A4?= =?UTF-8?q?=EB=AA=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이상 감지 시 Gemma 4(MLX localhost:8801)로 원인 분석 + 권장 조치 생성. Gemma 실패해도 rule 결과만으로 알림 전송 (graceful degradation). Co-Authored-By: Claude Opus 4.6 (1M context) --- infra/agent.py | 55 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/infra/agent.py b/infra/agent.py index ef74a46..cf9f303 100644 --- a/infra/agent.py +++ b/infra/agent.py @@ -3,10 +3,9 @@ 5분마다 실행되어: 1. core/ 함수로 상태 수집 2. Rule 기반 1차 판정 (threshold/패턴) -3. 이상 감지 시 → 시놀로지 Chat 알림 -4. 로그 → stdout (launchd가 캡처) - -Gemma 4는 Phase 2.1에서 추가 (알림 메시지 자연어 생성). +3. 이상 감지 시 → Gemma 4가 자연어 설명 생성 +4. 시놀로지 Chat 알림 (rule 요약 + Gemma 설명) +5. 로그 → stdout (launchd가 캡처) """ from __future__ import annotations @@ -54,6 +53,40 @@ IGNORED_CONTAINERS = { # Services to health-check HEALTH_SERVICES = ["document-server", "mlx", "ollama-gpu"] +# Gemma 4 — Mac mini MLX proxy (localhost on Mac mini) +GEMMA_URL = "http://localhost:8801/v1/chat/completions" +GEMMA_MODEL = "mlx-community/gemma-4-26b-a4b-it-8bit" +GEMMA_TIMEOUT = 30 # seconds + + +# --- Gemma --- + +async def generate_explanation(alerts: list[str]) -> str: + """Ask Gemma 4 to explain alerts and suggest actions. Returns empty on failure.""" + prompt = ( + "당신은 서버 인프라 모니터링 AI입니다. " + "아래 이상 항목들을 분석해서 간결하게 설명하고 권장 조치를 알려주세요.\n\n" + "이상 항목:\n" + "\n".join(f"- {a}" for a in alerts) + "\n\n" + "형식: 1~3문장으로 원인 분석 + 권장 조치. 한국어로." + ) + try: + async with httpx.AsyncClient(timeout=GEMMA_TIMEOUT) as client: + resp = await client.post( + GEMMA_URL, + json={ + "model": GEMMA_MODEL, + "messages": [{"role": "user", "content": prompt}], + "max_tokens": 200, + "temperature": 0.3, + }, + ) + if resp.status_code == 200: + data = resp.json() + return data["choices"][0]["message"]["content"].strip() + except Exception: + log.debug("Gemma 설명 생성 실패 — rule 결과만 전송", exc_info=True) + return "" + # --- Alert --- @@ -179,8 +212,18 @@ async def run_checks() -> None: for a in all_alerts: log.warning(" - %s", a) - # Send combined alert - message = f"이상 감지 {len(all_alerts)}건:\n" + "\n".join(f"- {a}" for a in all_alerts) + # Gemma 2nd: generate explanation + explanation = await generate_explanation(all_alerts) + if explanation: + log.info("Gemma 설명: %s", explanation[:100]) + + # Build alert message + lines = [f"이상 감지 {len(all_alerts)}건:"] + lines.extend(f"- {a}" for a in all_alerts) + if explanation: + lines.append(f"\n분석: {explanation}") + message = "\n".join(lines) + await send_alert(message) else: log.info("전체 정상")