fix(hermes): ANTHROPIC_API_KEY 注入 + solver 信心度修法 A + 12-Agent 治理文件
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
- nl_gateway.py: ClaudeAgentOptions 透過 env= 注入 ANTHROPIC_API_KEY(CLAUDE_API_KEY alias), 修復 SDK 找不到 API key 的問題(SDK 讀 ANTHROPIC_API_KEY,K8s secret 名稱是 CLAUDE_API_KEY) - solver_agent.py: 修法 A — kubectl_command 欄位優先路徑,OpenClaw Nemo 回傳完整指令時 不再被語意合成壓縮 confidence(0.9 → min(0.5) 的 bug),9 tests pass - AGENTS.md: Codex CLI 對應版 CLAUDE.md(Codex Session 啟動用) - docs/12-agent-game-rules.md: 12-Agent 任務判型 + 主責/協作派工 + 9 skills 對照(v1.0) - .agents/skills/06-awoooi-monorepo-master.md: v1.6,新增 12-agent 協作治理章節 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -317,6 +317,26 @@ def _extract_candidates(parsed: dict[str, Any]) -> list[CandidateAction]:
|
||||
risk_to_blast = {"critical": 60, "high": 40, "medium": 25, "low": 10}
|
||||
blast = risk_to_blast.get(risk_level.lower(), 30)
|
||||
|
||||
# 2026-04-24 ogt + Claude Sonnet 4.6: 修復信心度被語意合成硬壓問題
|
||||
# 根因:OpenClaw NIM 回傳 action_title(自然語言)+ kubectl_command(完整指令)
|
||||
# 舊邏輯:action_title 無 kubectl → 語意合成 → min(confidence, 0.5) 壓到 0.5
|
||||
# 修法 A:優先採用 kubectl_command 欄位,保留原始 confidence(如 0.9)
|
||||
kubectl_cmd = str(parsed.get("kubectl_command", "")).strip()
|
||||
if kubectl_cmd and kubectl_cmd.startswith("kubectl"):
|
||||
logger.debug(
|
||||
"solver_nemo_kubectl_command_used",
|
||||
action_title=action_title[:80],
|
||||
kubectl_command=kubectl_cmd[:80],
|
||||
confidence=confidence,
|
||||
)
|
||||
return [CandidateAction(
|
||||
action=kubectl_cmd[:200],
|
||||
blast_radius=blast,
|
||||
rollback_cost=20,
|
||||
confidence=confidence,
|
||||
rationale=f"OpenClaw Nemo: {action_title[:80]}",
|
||||
)]
|
||||
|
||||
if "kubectl" in action_title.lower():
|
||||
if action_title and confidence > 0:
|
||||
return [CandidateAction(
|
||||
|
||||
@@ -16,6 +16,7 @@ from claude_agent_sdk import query, ClaudeAgentOptions
|
||||
from claude_agent_sdk.types import ResultMessage
|
||||
from sqlalchemy import text
|
||||
|
||||
from src.core.config import settings
|
||||
from src.core.redis_client import get_redis
|
||||
from src.db.base import get_db_context
|
||||
from src.hermes.agent_loader import get_agent_system_prompt
|
||||
@@ -225,11 +226,15 @@ async def process_nl_message(
|
||||
success = False
|
||||
error_type: str | None = None
|
||||
try:
|
||||
sdk_env: dict[str, str] = {}
|
||||
if settings.CLAUDE_API_KEY:
|
||||
sdk_env["ANTHROPIC_API_KEY"] = settings.CLAUDE_API_KEY
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt=system_prompt,
|
||||
max_turns=3,
|
||||
permission_mode="dontAsk",
|
||||
max_budget_usd=_HERMES_BUDGET_USD,
|
||||
env=sdk_env,
|
||||
)
|
||||
result_text = ""
|
||||
async for event in query(prompt=prompt_with_ctx, options=options):
|
||||
|
||||
0
apps/api/tests/__init__.py
Normal file
0
apps/api/tests/__init__.py
Normal file
0
apps/api/tests/agents/__init__.py
Normal file
0
apps/api/tests/agents/__init__.py
Normal file
159
apps/api/tests/agents/test_solver_agent.py
Normal file
159
apps/api/tests/agents/test_solver_agent.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
solver_agent._extract_candidates 單元測試
|
||||
|
||||
2026-04-24 ogt + Claude Sonnet 4.6: 修法 A — kubectl_command 優先路徑驗證
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 確保 src 可找到
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../"))
|
||||
|
||||
import pytest
|
||||
|
||||
from src.agents.solver_agent import _extract_candidates
|
||||
|
||||
|
||||
class TestExtractCandidatesNemoFormat:
|
||||
"""OpenClaw Nemo 格式解析測試"""
|
||||
|
||||
def test_kubectl_command_field_preserves_confidence(self):
|
||||
"""修法 A 核心:kubectl_command 存在時,confidence 不被壓縮"""
|
||||
parsed = {
|
||||
"action_title": "重啟 Crash Looping Pod",
|
||||
"kubectl_command": "kubectl rollout restart deployment/awoooi-api -n awoooi-prod",
|
||||
"confidence": 0.9,
|
||||
"risk_level": "medium",
|
||||
}
|
||||
result = _extract_candidates(parsed)
|
||||
|
||||
assert len(result) == 1
|
||||
c = result[0]
|
||||
assert c.confidence == 0.9, f"期望 0.9,實際 {c.confidence}"
|
||||
assert c.action == "kubectl rollout restart deployment/awoooi-api -n awoooi-prod"
|
||||
assert "OpenClaw Nemo" in c.rationale
|
||||
assert "重啟 Crash Looping Pod" in c.rationale
|
||||
|
||||
def test_kubectl_command_field_high_confidence(self):
|
||||
"""kubectl_command 存在,confidence 0.85 仍完整保留"""
|
||||
parsed = {
|
||||
"action_title": "Scale Down Pod Count",
|
||||
"kubectl_command": "kubectl scale deployment/awoooi-api --replicas=2 -n awoooi-prod",
|
||||
"confidence": 0.85,
|
||||
"risk_level": "low",
|
||||
}
|
||||
result = _extract_candidates(parsed)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].confidence == 0.85
|
||||
assert result[0].blast_radius == 10 # risk_level=low → blast=10
|
||||
|
||||
def test_kubectl_command_field_takes_priority_over_synthesis(self):
|
||||
"""kubectl_command 存在時,不走語意合成(不被 min(0.5) 壓)"""
|
||||
parsed = {
|
||||
"action_title": "重啟服務", # 若無 kubectl_command,會走語意合成被壓到 0.5
|
||||
"kubectl_command": "kubectl rollout restart deployment/api -n awoooi-prod",
|
||||
"confidence": 0.9,
|
||||
"risk_level": "medium",
|
||||
}
|
||||
result = _extract_candidates(parsed)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].confidence == 0.9, "kubectl_command 路徑不應觸發 min(confidence, 0.5)"
|
||||
|
||||
def test_no_kubectl_command_action_title_has_kubectl(self):
|
||||
"""舊路徑向後相容:action_title 本身含 kubectl,無 kubectl_command"""
|
||||
parsed = {
|
||||
"action_title": "kubectl rollout restart deployment -n awoooi-prod",
|
||||
"confidence": 0.8,
|
||||
"risk_level": "medium",
|
||||
}
|
||||
result = _extract_candidates(parsed)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].confidence == 0.8
|
||||
assert "kubectl rollout restart" in result[0].action
|
||||
|
||||
def test_no_kubectl_command_synthesis_caps_confidence(self):
|
||||
"""語意合成備援路徑:confidence 仍被 min(0.5) 壓制(預期行為)"""
|
||||
parsed = {
|
||||
"action_title": "重啟服務", # 無 kubectl_command,觸發語意合成
|
||||
"confidence": 0.9,
|
||||
"risk_level": "medium",
|
||||
}
|
||||
result = _extract_candidates(parsed)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].confidence == 0.5, "語意合成路徑 confidence 必須被壓到 0.5"
|
||||
assert "[語意合成]" in result[0].rationale
|
||||
|
||||
def test_kubectl_command_empty_string_falls_through(self):
|
||||
"""kubectl_command 為空字串時,回落到既有邏輯"""
|
||||
parsed = {
|
||||
"action_title": "重啟服務",
|
||||
"kubectl_command": "",
|
||||
"confidence": 0.9,
|
||||
"risk_level": "medium",
|
||||
}
|
||||
result = _extract_candidates(parsed)
|
||||
|
||||
# 空 kubectl_command → 走語意合成 → confidence 被壓
|
||||
assert len(result) == 1
|
||||
assert result[0].confidence == 0.5
|
||||
|
||||
def test_kubectl_command_not_starting_with_kubectl_falls_through(self):
|
||||
"""kubectl_command 非 kubectl 開頭(可能是雜訊),回落到既有邏輯"""
|
||||
parsed = {
|
||||
"action_title": "重啟服務",
|
||||
"kubectl_command": "helm rollback awoooi-api",
|
||||
"confidence": 0.9,
|
||||
"risk_level": "medium",
|
||||
}
|
||||
result = _extract_candidates(parsed)
|
||||
|
||||
# helm 指令不被採用 → 語意合成 → confidence 被壓
|
||||
assert len(result) == 1
|
||||
assert result[0].confidence == 0.5
|
||||
|
||||
def test_no_action_title_no_candidates_uses_standard_path(self):
|
||||
"""標準 candidates 陣列格式不受影響"""
|
||||
parsed = {
|
||||
"candidates": [
|
||||
{
|
||||
"action": "kubectl rollout restart deployment/api -n awoooi-prod",
|
||||
"blast_radius": 25,
|
||||
"rollback_cost": 20,
|
||||
"confidence": 0.85,
|
||||
"rationale": "重啟修復 OOM",
|
||||
}
|
||||
]
|
||||
}
|
||||
result = _extract_candidates(parsed)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].confidence == 0.85
|
||||
assert result[0].blast_radius == 25
|
||||
|
||||
def test_risk_level_blast_radius_mapping(self):
|
||||
"""risk_level → blast_radius 映射正確"""
|
||||
cases = [
|
||||
("critical", 60),
|
||||
("high", 40),
|
||||
("medium", 25),
|
||||
("low", 10),
|
||||
("unknown", 30), # 預設
|
||||
]
|
||||
for risk_level, expected_blast in cases:
|
||||
parsed = {
|
||||
"action_title": "fix",
|
||||
"kubectl_command": "kubectl get pods -n awoooi-prod",
|
||||
"confidence": 0.9,
|
||||
"risk_level": risk_level,
|
||||
}
|
||||
result = _extract_candidates(parsed)
|
||||
assert result[0].blast_radius == expected_blast, (
|
||||
f"risk_level={risk_level} → 期望 blast={expected_blast},實際 {result[0].blast_radius}"
|
||||
)
|
||||
Reference in New Issue
Block a user