feat(awooop): expose mcp evidence details on incidents
This commit is contained in:
@@ -1151,6 +1151,67 @@ def _select_status_chain_source_id(
|
||||
return incident_ids[0] if incident_ids else latest_incident_id or None
|
||||
|
||||
|
||||
def _status_chain_mcp_section(truth_chain: dict[str, Any] | None) -> dict[str, Any]:
|
||||
mcp = truth_chain.get("mcp") if isinstance(truth_chain, dict) else {}
|
||||
if not isinstance(mcp, dict):
|
||||
mcp = {}
|
||||
gateway = mcp.get("awooop_gateway") if isinstance(mcp.get("awooop_gateway"), dict) else {}
|
||||
legacy = mcp.get("legacy") if isinstance(mcp.get("legacy"), dict) else {}
|
||||
|
||||
top_tools: list[dict[str, Any]] = []
|
||||
seen_tools: set[str] = set()
|
||||
for source, summary in (("gateway", gateway), ("legacy", legacy)):
|
||||
by_tool = summary.get("by_tool") if isinstance(summary, dict) else []
|
||||
if not isinstance(by_tool, list):
|
||||
continue
|
||||
for item in by_tool:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
tool_name = str(item.get("tool_name") or "unknown").strip() or "unknown"
|
||||
key = f"{source}:{tool_name}"
|
||||
if key in seen_tools:
|
||||
continue
|
||||
seen_tools.add(key)
|
||||
top_tools.append({
|
||||
"source": source,
|
||||
"tool_name": tool_name,
|
||||
"total": (
|
||||
_safe_int(item.get("total"))
|
||||
or _safe_int(item.get("success"))
|
||||
+ _safe_int(item.get("failed"))
|
||||
+ _safe_int(item.get("blocked"))
|
||||
),
|
||||
"success": _safe_int(item.get("success")),
|
||||
"failed": _safe_int(item.get("failed")),
|
||||
"blocked": _safe_int(item.get("blocked")),
|
||||
"last_error": item.get("last_error"),
|
||||
})
|
||||
if len(top_tools) >= 5:
|
||||
break
|
||||
if len(top_tools) >= 5:
|
||||
break
|
||||
|
||||
return {
|
||||
"gateway": {
|
||||
"total": _safe_int(gateway.get("total")),
|
||||
"success": _safe_int(gateway.get("success")),
|
||||
"failed": _safe_int(gateway.get("failed")),
|
||||
"blocked": _safe_int(gateway.get("blocked")),
|
||||
"first_class_total": _safe_int(gateway.get("first_class_total")),
|
||||
"legacy_bridge_total": _safe_int(gateway.get("legacy_bridge_total")),
|
||||
"policy_enforced_total": _safe_int(gateway.get("policy_enforced_total")),
|
||||
"stage": gateway.get("stage"),
|
||||
"stage_status": gateway.get("stage_status"),
|
||||
},
|
||||
"legacy": {
|
||||
"total": _safe_int(legacy.get("total")),
|
||||
"success": _safe_int(legacy.get("success")),
|
||||
"failed": _safe_int(legacy.get("failed")),
|
||||
},
|
||||
"top_tools": top_tools,
|
||||
}
|
||||
|
||||
|
||||
def _build_awooop_status_chain(
|
||||
*,
|
||||
incident_ids: list[str],
|
||||
@@ -1228,6 +1289,7 @@ def _build_awooop_status_chain(
|
||||
):
|
||||
needs_human = True
|
||||
|
||||
mcp_section = _status_chain_mcp_section(truth_chain)
|
||||
blockers = [
|
||||
str(item)
|
||||
for item in [
|
||||
@@ -1269,6 +1331,7 @@ def _build_awooop_status_chain(
|
||||
"incident": latest.get("writes_incident_state"),
|
||||
"auto_repair": latest.get("writes_auto_repair_result"),
|
||||
},
|
||||
"mcp": mcp_section,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -470,6 +470,40 @@ def test_awooop_status_chain_marks_verified_repair() -> None:
|
||||
},
|
||||
"blockers": [],
|
||||
},
|
||||
"mcp": {
|
||||
"awooop_gateway": {
|
||||
"total": 2,
|
||||
"success": 1,
|
||||
"failed": 1,
|
||||
"blocked": 0,
|
||||
"first_class_total": 2,
|
||||
"legacy_bridge_total": 0,
|
||||
"policy_enforced_total": 2,
|
||||
"stage": "provider_failed_after_gateway",
|
||||
"stage_status": "failed",
|
||||
"by_tool": [
|
||||
{
|
||||
"tool_name": "prometheus.query",
|
||||
"total": 2,
|
||||
"success": 1,
|
||||
"failed": 1,
|
||||
"blocked": 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
"legacy": {
|
||||
"total": 1,
|
||||
"success": 1,
|
||||
"failed": 0,
|
||||
"by_tool": [
|
||||
{
|
||||
"tool_name": "ssh_host",
|
||||
"success": 1,
|
||||
"failed": 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
remediation_history={
|
||||
"total": 1,
|
||||
@@ -492,6 +526,11 @@ def test_awooop_status_chain_marks_verified_repair() -> None:
|
||||
assert chain["needs_human"] is False
|
||||
assert chain["next_step"] == "monitor_for_regression"
|
||||
assert chain["evidence"]["latest_route"] == "auto_repair_executor/rollout_restart/write"
|
||||
assert chain["mcp"]["gateway"]["success"] == 1
|
||||
assert chain["mcp"]["gateway"]["failed"] == 1
|
||||
assert chain["mcp"]["gateway"]["policy_enforced_total"] == 2
|
||||
assert chain["mcp"]["legacy"]["total"] == 1
|
||||
assert chain["mcp"]["top_tools"][0]["tool_name"] == "prometheus.query"
|
||||
|
||||
|
||||
def test_awooop_status_chain_marks_read_only_manual_gate() -> None:
|
||||
|
||||
@@ -431,6 +431,7 @@
|
||||
"flowEvidenceOps": "Ops {count}",
|
||||
"flowEvidenceKm": "KM {count}",
|
||||
"flowEvidenceRepair": "Repair {count}",
|
||||
"flowMcpDetail": "MCP detail: Gateway success {success} / failed {failed} / blocked {blocked}; first-class {firstClass}; legacy {legacy}; tools {tools}",
|
||||
"flowTruthChainCurrent": "{stage} / {status}",
|
||||
"flowComplete": "Complete",
|
||||
"flowStages": {
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"flowEvidenceOps": "操作 {count}",
|
||||
"flowEvidenceKm": "KM {count}",
|
||||
"flowEvidenceRepair": "修復 {count}",
|
||||
"flowMcpDetail": "MCP 明細:Gateway 成功 {success} / 失敗 {failed} / 阻擋 {blocked};一級治理 {firstClass};Legacy {legacy};工具 {tools}",
|
||||
"flowTruthChainCurrent": "{stage} / {status}",
|
||||
"flowComplete": "已完成",
|
||||
"flowStages": {
|
||||
|
||||
@@ -35,6 +35,33 @@ export interface AwoooPStatusChain {
|
||||
incident?: boolean | null;
|
||||
auto_repair?: boolean | null;
|
||||
};
|
||||
mcp?: {
|
||||
gateway?: {
|
||||
total?: number | null;
|
||||
success?: number | null;
|
||||
failed?: number | null;
|
||||
blocked?: number | null;
|
||||
first_class_total?: number | null;
|
||||
legacy_bridge_total?: number | null;
|
||||
policy_enforced_total?: number | null;
|
||||
stage?: string | null;
|
||||
stage_status?: string | null;
|
||||
};
|
||||
legacy?: {
|
||||
total?: number | null;
|
||||
success?: number | null;
|
||||
failed?: number | null;
|
||||
};
|
||||
top_tools?: Array<{
|
||||
source?: string | null;
|
||||
tool_name?: string | null;
|
||||
total?: number | null;
|
||||
success?: number | null;
|
||||
failed?: number | null;
|
||||
blocked?: number | null;
|
||||
last_error?: string | null;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
function toneClass(chain?: AwoooPStatusChain | null) {
|
||||
|
||||
@@ -335,6 +335,22 @@ export function IncidentCard({ incident, decision, statusChain, onApprovalChange
|
||||
active: (statusChain?.evidence?.auto_repair_records ?? 0) > 0,
|
||||
},
|
||||
] : []
|
||||
const mcpTopTools = (statusChain?.mcp?.top_tools ?? []).slice(0, 3).map(tool => {
|
||||
const toolName = chainValue(tool.tool_name)
|
||||
return `${toolName} ${tool.success ?? 0}/${tool.failed ?? 0}/${tool.blocked ?? 0}`
|
||||
}).join(', ')
|
||||
const mcpGateway = statusChain?.mcp?.gateway
|
||||
const mcpLegacy = statusChain?.mcp?.legacy
|
||||
const statusChainMcpDetail = hasTruthChain
|
||||
? t('flowMcpDetail', {
|
||||
success: mcpGateway?.success ?? 0,
|
||||
failed: mcpGateway?.failed ?? 0,
|
||||
blocked: mcpGateway?.blocked ?? 0,
|
||||
firstClass: mcpGateway?.first_class_total ?? 0,
|
||||
legacy: mcpLegacy?.total ?? 0,
|
||||
tools: mcpTopTools || '--',
|
||||
})
|
||||
: null
|
||||
|
||||
const serviceName = incident.affected_services?.[0] ?? '--'
|
||||
const duration = formatDuration(incident.created_at)
|
||||
@@ -606,6 +622,10 @@ export function IncidentCard({ incident, decision, statusChain, onApprovalChange
|
||||
</strong>
|
||||
))}
|
||||
</span>
|
||||
<span style={{ color: '#b0ad9f' }}>/</span>
|
||||
<span data-testid="incident-mcp-evidence" style={{ color: '#555550' }}>
|
||||
{statusChainMcpDetail}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user