fix(alerts): 補齊處置結果與人工通知契約
This commit is contained in:
230
apps/api/src/services/operator_outcome.py
Normal file
230
apps/api/src/services/operator_outcome.py
Normal file
@@ -0,0 +1,230 @@
|
||||
"""Operator-facing alert outcome contract.
|
||||
|
||||
This module is intentionally generic: it converts the existing truth-chain
|
||||
stage, automation quality verdict, and remediation evidence state into one
|
||||
small contract that Telegram, AwoooP, and result notifications can all share.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
_ACTION_REQUIRED_CHANNELS = ("telegram_sre_war_room", "awooop_operator_console")
|
||||
_RESULT_ONLY_CHANNELS = ("telegram_result_reply", "awooop_operator_console")
|
||||
|
||||
|
||||
def _safe_int(value: Any) -> int:
|
||||
try:
|
||||
return int(value or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def _first_text(values: list[Any]) -> str | None:
|
||||
for value in values:
|
||||
if value:
|
||||
return str(value)
|
||||
return None
|
||||
|
||||
|
||||
def _build_notification(
|
||||
*,
|
||||
mode: str,
|
||||
channels: tuple[str, ...],
|
||||
reason: str,
|
||||
source_id: str | None,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"mode": mode,
|
||||
"channels": list(channels),
|
||||
"reason": reason,
|
||||
"source_id": source_id,
|
||||
"telegram": (
|
||||
"reply_to_original_or_standalone_action_required"
|
||||
if mode == "action_required"
|
||||
else "reply_to_original_or_standalone_result"
|
||||
),
|
||||
"awooop": "status_chain_panel",
|
||||
}
|
||||
|
||||
|
||||
def build_operator_outcome(
|
||||
*,
|
||||
truth_status: dict[str, Any] | None = None,
|
||||
automation_quality: dict[str, Any] | None = None,
|
||||
remediation_state: str | None = None,
|
||||
fetch_error: str | None = None,
|
||||
source_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a normalized operator outcome for an alert/incident.
|
||||
|
||||
The output deliberately answers three questions:
|
||||
1. What happened?
|
||||
2. Does a human need to intervene?
|
||||
3. How will that human be notified / where should they act?
|
||||
"""
|
||||
truth_status = truth_status or {}
|
||||
automation_quality = automation_quality or {}
|
||||
facts = automation_quality.get("facts")
|
||||
if not isinstance(facts, dict):
|
||||
facts = {}
|
||||
|
||||
verdict = str(automation_quality.get("verdict") or "unknown")
|
||||
stage = str(truth_status.get("current_stage") or "unknown")
|
||||
stage_status = str(truth_status.get("stage_status") or "unknown")
|
||||
blockers = [
|
||||
str(item)
|
||||
for item in [
|
||||
*(truth_status.get("blockers") if isinstance(truth_status.get("blockers"), list) else []),
|
||||
*(automation_quality.get("blockers") if isinstance(automation_quality.get("blockers"), list) else []),
|
||||
]
|
||||
if item
|
||||
]
|
||||
verification = str(facts.get("verification_result") or "missing")
|
||||
has_repair_execution = _safe_int(facts.get("effective_execution_records")) > 0 or _safe_int(
|
||||
facts.get("auto_repair_execution_records")
|
||||
) > 0
|
||||
has_nonrepair_operation = (
|
||||
_safe_int(facts.get("automation_operation_records")) > 0
|
||||
and not has_repair_execution
|
||||
)
|
||||
needs_human_from_truth = bool(truth_status.get("needs_human"))
|
||||
first_blocker = _first_text(blockers)
|
||||
|
||||
if fetch_error:
|
||||
state = "truth_chain_unavailable"
|
||||
severity = "warning"
|
||||
needs_human = True
|
||||
next_action = "open_awooop_and_review_source_records"
|
||||
summary = "真相鏈查詢失敗,需人工確認處置結果"
|
||||
reason = str(fetch_error)[:240]
|
||||
elif verdict == "auto_repaired_verified":
|
||||
state = "completed_verified"
|
||||
severity = "success"
|
||||
needs_human = False
|
||||
next_action = "monitor_for_regression"
|
||||
summary = "已驗證自動修復完成"
|
||||
reason = "execution_and_verification_succeeded"
|
||||
elif verdict == "execution_failed" or stage == "execution_failed":
|
||||
state = "execution_failed_manual_required"
|
||||
severity = "critical"
|
||||
needs_human = True
|
||||
next_action = "manual_fix_or_rollback"
|
||||
summary = "執行失敗,需人工介入"
|
||||
reason = first_blocker or "execution_failed"
|
||||
elif verdict == "manual_required_diagnostic_only" or has_nonrepair_operation:
|
||||
state = "diagnostic_only_manual_review"
|
||||
severity = "warning"
|
||||
needs_human = True
|
||||
next_action = "manual_review_or_collect_repair_evidence"
|
||||
summary = "只完成診斷/觀察,尚未證明修復"
|
||||
reason = first_blocker or "diagnostic_or_audit_only"
|
||||
elif verdict == "auto_repaired_verification_degraded":
|
||||
state = "verification_degraded_manual_required"
|
||||
severity = "warning"
|
||||
needs_human = True
|
||||
next_action = "manual_verify_or_repair"
|
||||
summary = "已執行但驗證退化,需人工確認"
|
||||
reason = first_blocker or f"verification={verification}"
|
||||
elif verdict == "execution_unverified" or (
|
||||
has_repair_execution and verification == "missing"
|
||||
):
|
||||
state = "execution_unverified_manual_required"
|
||||
severity = "warning"
|
||||
needs_human = True
|
||||
next_action = "run_or_review_post_execution_verification"
|
||||
summary = "已執行但缺少驗證結果,需人工確認"
|
||||
reason = first_blocker or "execution_without_verification_result"
|
||||
elif verdict == "manual_required_no_action":
|
||||
state = "no_action_manual_review"
|
||||
severity = "warning"
|
||||
needs_human = True
|
||||
next_action = "manual_review_no_action_decision"
|
||||
summary = "AI 選擇不執行修復,需人工判斷是否接手"
|
||||
reason = first_blocker or "no_action_or_observe"
|
||||
elif remediation_state == "read_only":
|
||||
state = "read_only_dry_run_manual_gate"
|
||||
severity = "warning"
|
||||
needs_human = True
|
||||
next_action = "approve_or_escalate_from_awooop"
|
||||
summary = "只讀試跑完成,等待人工放行或轉交"
|
||||
reason = first_blocker or "read_only_dry_run"
|
||||
elif remediation_state == "write_observed":
|
||||
state = "write_observed_manual_review"
|
||||
severity = "critical"
|
||||
needs_human = True
|
||||
next_action = "review_write_evidence"
|
||||
summary = "補救證據出現寫入旗標,需人工確認"
|
||||
reason = first_blocker or "write_observed"
|
||||
elif remediation_state in {"blocked", "fetch_failed"}:
|
||||
state = "blocked_manual_required"
|
||||
severity = "critical" if remediation_state == "blocked" else "warning"
|
||||
needs_human = True
|
||||
next_action = "manual_investigation"
|
||||
summary = "自動化流程受阻,需人工處理"
|
||||
reason = first_blocker or remediation_state
|
||||
elif verdict == "approval_required" or stage == "approval_required":
|
||||
state = "approval_required"
|
||||
severity = "warning"
|
||||
needs_human = True
|
||||
next_action = "approve_reject_or_escalate"
|
||||
summary = "等待人工審批,尚未執行"
|
||||
reason = first_blocker or "pending_human_approval"
|
||||
elif needs_human_from_truth:
|
||||
state = "manual_required"
|
||||
severity = "warning"
|
||||
needs_human = True
|
||||
next_action = "manual_investigation"
|
||||
summary = "真相鏈判定需人工介入"
|
||||
reason = first_blocker or f"{stage}/{stage_status}"
|
||||
elif verdict in {"observed_not_executed", "received_only"}:
|
||||
state = "observed_not_executed"
|
||||
severity = "info"
|
||||
needs_human = False
|
||||
next_action = "collect_evidence_or_wait"
|
||||
summary = "已收到/觀測,尚未進入修復執行"
|
||||
reason = first_blocker or verdict
|
||||
else:
|
||||
state = "unknown_pending_observation"
|
||||
severity = "warning"
|
||||
needs_human = bool(blockers)
|
||||
next_action = "review_status_chain"
|
||||
summary = "處置結果尚未形成明確結論"
|
||||
reason = first_blocker or f"{verdict}:{stage}/{stage_status}"
|
||||
|
||||
mode = "action_required" if needs_human else "result_only"
|
||||
channels = _ACTION_REQUIRED_CHANNELS if needs_human else _RESULT_ONLY_CHANNELS
|
||||
return {
|
||||
"schema_version": "operator_outcome_v1",
|
||||
"state": state,
|
||||
"severity": severity,
|
||||
"summary_zh": summary,
|
||||
"needs_human": needs_human,
|
||||
"human_action_required": needs_human,
|
||||
"human_action_reason": reason,
|
||||
"next_action": next_action,
|
||||
"notification": _build_notification(
|
||||
mode=mode,
|
||||
channels=channels,
|
||||
reason=reason,
|
||||
source_id=source_id,
|
||||
),
|
||||
"evidence": {
|
||||
"verdict": verdict,
|
||||
"current_stage": stage,
|
||||
"stage_status": stage_status,
|
||||
"verification": verification,
|
||||
"auto_repair_execution_records": _safe_int(
|
||||
facts.get("auto_repair_execution_records")
|
||||
),
|
||||
"effective_execution_records": _safe_int(
|
||||
facts.get("effective_execution_records")
|
||||
),
|
||||
"automation_operation_records": _safe_int(
|
||||
facts.get("automation_operation_records")
|
||||
),
|
||||
"mcp_gateway_total": _safe_int(facts.get("mcp_gateway_total")),
|
||||
"knowledge_entries": _safe_int(facts.get("knowledge_entries")),
|
||||
},
|
||||
"blockers": blockers[:8],
|
||||
}
|
||||
Reference in New Issue
Block a user