feat(mcp-phase4a): NemoClaw second opinion — 信心 < 0.7 觸發 deepseek-r1:14b 複審
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
- _nemoclaw_second_opinion(): 呼叫 Ollama 188 deepseek-r1:14b 做獨立推理 - 解析 <think>...</think> CoT 格式,只取正文 - 30s timeout,失敗靜默降級 - 輸出截斷 300 字 - _dual_engine_analyze(): LLM 信心 < 0.7 時非同步觸發 second opinion - 結果附加到 proposal_data["advisory_note"] - _push_decision_to_telegram(): advisory_note 以 NemoClaw bot 身分追加訊息 - 格式: "NemoClaw 第二意見 (信心=0.xx)" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -240,6 +240,17 @@ async def _push_decision_to_telegram(
|
||||
# 非同步執行,不阻塞主流程
|
||||
asyncio.create_task(_send_log_summary(incident))
|
||||
|
||||
# MCP Phase 4a: NemoClaw second opinion (2026-04-11 Claude Sonnet 4.6)
|
||||
# 若 proposal_data 有 advisory_note,用 NemoClaw bot 身分追加一條訊息
|
||||
_advisory_note = proposal_data.get("advisory_note", "")
|
||||
if _advisory_note:
|
||||
asyncio.create_task(
|
||||
gateway.send_as_nemotron(
|
||||
f"🤔 <b>NemoClaw 第二意見</b> (信心={confidence:.2f})\n"
|
||||
f"<i>{_advisory_note}</i>"
|
||||
)
|
||||
)
|
||||
|
||||
# 🔴 發送成功後設置去重 key (TTL 10 分鐘)
|
||||
await redis.setex(dedup_key, 600, "1")
|
||||
|
||||
@@ -259,6 +270,62 @@ async def _push_decision_to_telegram(
|
||||
)
|
||||
|
||||
|
||||
async def _nemoclaw_second_opinion(incident: "Incident", primary_result: dict) -> str | None:
|
||||
"""
|
||||
MCP Phase 4a: NemoClaw second opinion — 信心 < 0.7 時觸發
|
||||
============================================================
|
||||
用 deepseek-r1:14b (Ollama 188) 對同一份資料做獨立推理,
|
||||
輸出純文字 advisory_note,不執行任何操作。
|
||||
|
||||
2026-04-11 Claude Sonnet 4.6 Asia/Taipei
|
||||
"""
|
||||
try:
|
||||
from src.core.config import settings
|
||||
import httpx as _httpx
|
||||
|
||||
ollama_url = getattr(settings, "OLLAMA_URL", "http://192.168.0.188:11434")
|
||||
model = "deepseek-r1:14b"
|
||||
|
||||
signals_summary = ""
|
||||
if incident.signals:
|
||||
lbl = incident.signals[0].labels
|
||||
signals_summary = (
|
||||
f"alertname={lbl.get('alertname','?')} "
|
||||
f"severity={lbl.get('severity','?')} "
|
||||
f"instance={lbl.get('instance','?')}"
|
||||
)
|
||||
|
||||
prompt = (
|
||||
f"你是資深 SRE,請對以下告警做獨立分析並提出建議。\n"
|
||||
f"告警摘要: {signals_summary}\n"
|
||||
f"受影響服務: {', '.join(incident.affected_services or [])}\n"
|
||||
f"主 AI 已提出: {primary_result.get('action','?')} "
|
||||
f"(信心={primary_result.get('confidence',0):.2f})\n"
|
||||
f"主 AI 根因: {primary_result.get('reasoning','')[:200]}\n\n"
|
||||
f"請用繁體中文,100 字以內,給出你的獨立判斷與建議(若同意主 AI 則說明原因)。"
|
||||
)
|
||||
|
||||
async with _httpx.AsyncClient(timeout=30.0) as client:
|
||||
resp = await client.post(
|
||||
f"{ollama_url}/api/generate",
|
||||
json={"model": model, "prompt": prompt, "stream": False},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
advisory = data.get("response", "").strip()
|
||||
# 截取 <think>...</think> 後的正文(deepseek-r1 CoT 格式)
|
||||
if "</think>" in advisory:
|
||||
advisory = advisory.split("</think>", 1)[-1].strip()
|
||||
|
||||
return advisory[:300] if advisory else None
|
||||
|
||||
except Exception as e:
|
||||
import structlog as _sl
|
||||
_sl.get_logger(__name__).debug("nemoclaw_second_opinion_error", error=str(e))
|
||||
return None
|
||||
|
||||
|
||||
async def _fetch_metrics_snapshot(incident: Incident) -> dict:
|
||||
"""
|
||||
ADR-071-I: 從 Prometheus 抓取與此 incident 相關的指標快照
|
||||
@@ -993,10 +1060,25 @@ class DecisionManager:
|
||||
provider=provider,
|
||||
kb_rag=bool(kb_context),
|
||||
)
|
||||
return {
|
||||
**llm_result,
|
||||
"source": f"llm_{provider}",
|
||||
}
|
||||
result = {**llm_result, "source": f"llm_{provider}"}
|
||||
|
||||
# MCP Phase 4a: 信心 < 0.7 → NemoClaw second opinion (2026-04-11 Claude Sonnet 4.6)
|
||||
_conf = float(result.get("confidence", 1.0))
|
||||
if _conf < 0.7:
|
||||
try:
|
||||
_advisory = await _nemoclaw_second_opinion(incident, result)
|
||||
if _advisory:
|
||||
result["advisory_note"] = _advisory
|
||||
logger.info(
|
||||
"nemoclaw_second_opinion_added",
|
||||
incident_id=incident.incident_id,
|
||||
confidence=_conf,
|
||||
)
|
||||
except Exception as _soe:
|
||||
logger.warning("nemoclaw_second_opinion_failed",
|
||||
incident_id=incident.incident_id, error=str(_soe))
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
|
||||
Reference in New Issue
Block a user