feat(awooop): show callback evidence capture status
This commit is contained in:
@@ -98,6 +98,7 @@ class CallbackReplyItem(BaseModel):
|
||||
persisted_awooop_status_chain: dict[str, Any] | None = None
|
||||
km_stale_completion_summary: dict[str, Any] | None = None
|
||||
persisted_km_stale_completion_summary: dict[str, Any] | None = None
|
||||
evidence_capture_status: dict[str, Any] | None = None
|
||||
run_detail_href: str | None = None
|
||||
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ _SOURCE_CORRELATION_PRE_WINDOW_HOURS = 2
|
||||
_KM_STALE_COMPLETION_CALLBACK_SCHEMA_VERSION = (
|
||||
"km_stale_owner_review_completion_callback_summary_v1"
|
||||
)
|
||||
_CALLBACK_EVIDENCE_CAPTURE_STATUS_SCHEMA_VERSION = "callback_evidence_capture_status_v1"
|
||||
|
||||
# =============================================================================
|
||||
# Tenants
|
||||
@@ -1027,6 +1028,56 @@ def _callback_reply_public_status(callback_reply: dict[str, Any]) -> str:
|
||||
}.get(raw_status, "observed")
|
||||
|
||||
|
||||
def _callback_reply_evidence_capture_status(
|
||||
*,
|
||||
callback_reply: Mapping[str, Any],
|
||||
persisted_awooop_status_chain: dict[str, Any] | None,
|
||||
persisted_km_stale_completion_summary: dict[str, Any] | None,
|
||||
event_at: Any,
|
||||
) -> dict[str, Any]:
|
||||
"""Explain whether callback-time evidence snapshots were persisted."""
|
||||
captured: list[str] = []
|
||||
missing: list[str] = []
|
||||
if persisted_awooop_status_chain:
|
||||
captured.append("awooop_status_chain")
|
||||
else:
|
||||
missing.append("awooop_status_chain")
|
||||
if persisted_km_stale_completion_summary:
|
||||
captured.append("km_stale_completion_summary")
|
||||
else:
|
||||
missing.append("km_stale_completion_summary")
|
||||
|
||||
if not missing:
|
||||
status_value = "captured"
|
||||
reason = "ok"
|
||||
next_action = "none"
|
||||
elif captured:
|
||||
status_value = "partial"
|
||||
reason = "partial_snapshot_rollout_transition"
|
||||
next_action = "press_telegram_detail_or_history_after_rollout"
|
||||
else:
|
||||
status_value = "not_captured"
|
||||
raw_status = str(callback_reply.get("status") or "")
|
||||
reason = (
|
||||
"callback_reply_delivery_failed_snapshot_missing"
|
||||
if raw_status == "callback_reply_failed"
|
||||
else "legacy_callback_before_snapshot_rollout"
|
||||
)
|
||||
next_action = "press_telegram_detail_or_history_after_rollout"
|
||||
|
||||
return {
|
||||
"schema_version": _CALLBACK_EVIDENCE_CAPTURE_STATUS_SCHEMA_VERSION,
|
||||
"status": status_value,
|
||||
"reason": reason,
|
||||
"action": str(callback_reply.get("action") or "").strip() or None,
|
||||
"captured": captured,
|
||||
"missing": missing,
|
||||
"snapshot_rollout": "t167_t169",
|
||||
"next_action": next_action,
|
||||
"event_at": event_at,
|
||||
}
|
||||
|
||||
|
||||
def _callback_reply_event_item(row: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""Convert one callback reply outbound row into a read-only evidence item."""
|
||||
callback_reply = _as_dict(row.get("callback_reply"))
|
||||
@@ -1036,6 +1087,12 @@ def _callback_reply_event_item(row: Mapping[str, Any]) -> dict[str, Any]:
|
||||
run_id = row.get("run_id")
|
||||
status_value = _callback_reply_public_status(callback_reply)
|
||||
event_at = row.get("sent_at") or row.get("queued_at")
|
||||
persisted_awooop_status_chain = _as_dict(
|
||||
row.get("persisted_awooop_status_chain"),
|
||||
) or None
|
||||
persisted_km_stale_completion_summary = _as_dict(
|
||||
row.get("persisted_km_stale_completion_summary"),
|
||||
) or None
|
||||
|
||||
return {
|
||||
"message_id": row.get("message_id"),
|
||||
@@ -1057,12 +1114,18 @@ def _callback_reply_event_item(row: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"agent_id": row.get("agent_id"),
|
||||
"run_created_at": row.get("run_created_at"),
|
||||
"callback_reply": callback_reply,
|
||||
"persisted_awooop_status_chain": _as_dict(
|
||||
row.get("persisted_awooop_status_chain"),
|
||||
) or None,
|
||||
"persisted_km_stale_completion_summary": _as_dict(
|
||||
row.get("persisted_km_stale_completion_summary"),
|
||||
) or None,
|
||||
"persisted_awooop_status_chain": persisted_awooop_status_chain,
|
||||
"persisted_km_stale_completion_summary": (
|
||||
persisted_km_stale_completion_summary
|
||||
),
|
||||
"evidence_capture_status": _callback_reply_evidence_capture_status(
|
||||
callback_reply=callback_reply,
|
||||
persisted_awooop_status_chain=persisted_awooop_status_chain,
|
||||
persisted_km_stale_completion_summary=(
|
||||
persisted_km_stale_completion_summary
|
||||
),
|
||||
event_at=event_at,
|
||||
),
|
||||
"run_detail_href": (
|
||||
f"/awooop/runs/{run_id}?project_id={project_id}"
|
||||
if run_id and project_id
|
||||
|
||||
@@ -407,6 +407,56 @@ def test_callback_reply_event_item_surfaces_run_link_and_human_flag() -> None:
|
||||
assert item["persisted_awooop_status_chain"]["repair_state"] == (
|
||||
"blocked_manual_required"
|
||||
)
|
||||
assert item["evidence_capture_status"]["status"] == "captured"
|
||||
assert item["evidence_capture_status"]["captured"] == [
|
||||
"awooop_status_chain",
|
||||
"km_stale_completion_summary",
|
||||
]
|
||||
assert item["evidence_capture_status"]["missing"] == []
|
||||
assert item["evidence_capture_status"]["next_action"] == "none"
|
||||
|
||||
|
||||
def test_callback_reply_event_item_marks_legacy_snapshot_missing() -> None:
|
||||
run_id = UUID("5c0306e0-591a-5445-9a33-80f499426b38")
|
||||
message_id = UUID("56cdb6ad-46a4-48f5-9d3b-b1ac9c0b2e92")
|
||||
|
||||
item = _callback_reply_event_item({
|
||||
"message_id": message_id,
|
||||
"run_id": run_id,
|
||||
"project_id": "awoooi",
|
||||
"channel_type": "telegram",
|
||||
"message_type": "final",
|
||||
"send_status": "sent",
|
||||
"send_error": None,
|
||||
"provider_message_id": "123",
|
||||
"queued_at": datetime(2026, 5, 18, 7, 31, 37),
|
||||
"sent_at": datetime(2026, 5, 18, 7, 31, 38),
|
||||
"triggered_by_state": "callback_reply",
|
||||
"content_preview": "事件詳情",
|
||||
"run_state": "completed",
|
||||
"agent_id": "legacy-telegram-gateway",
|
||||
"run_created_at": datetime(2026, 5, 18, 7, 30, 0),
|
||||
"callback_reply": {
|
||||
"status": "callback_reply_sent",
|
||||
"action": "detail",
|
||||
"incident_id": "INC-20260513-79ED5E",
|
||||
},
|
||||
"persisted_awooop_status_chain": None,
|
||||
"persisted_km_stale_completion_summary": None,
|
||||
})
|
||||
|
||||
capture_status = item["evidence_capture_status"]
|
||||
assert capture_status["schema_version"] == "callback_evidence_capture_status_v1"
|
||||
assert capture_status["status"] == "not_captured"
|
||||
assert capture_status["reason"] == "legacy_callback_before_snapshot_rollout"
|
||||
assert capture_status["missing"] == [
|
||||
"awooop_status_chain",
|
||||
"km_stale_completion_summary",
|
||||
]
|
||||
assert capture_status["captured"] == []
|
||||
assert capture_status["next_action"] == (
|
||||
"press_telegram_detail_or_history_after_rollout"
|
||||
)
|
||||
|
||||
|
||||
def test_list_callback_replies_response_preserves_callback_evidence() -> None:
|
||||
@@ -513,6 +563,20 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None:
|
||||
"automation_state": "manual_owner_review_required",
|
||||
},
|
||||
},
|
||||
"evidence_capture_status": {
|
||||
"schema_version": "callback_evidence_capture_status_v1",
|
||||
"status": "captured",
|
||||
"reason": "ok",
|
||||
"action": "detail",
|
||||
"captured": [
|
||||
"awooop_status_chain",
|
||||
"km_stale_completion_summary",
|
||||
],
|
||||
"missing": [],
|
||||
"snapshot_rollout": "t167_t169",
|
||||
"next_action": "none",
|
||||
"event_at": datetime(2026, 5, 18, 7, 31, 37),
|
||||
},
|
||||
"run_detail_href": (
|
||||
"/awooop/runs/5c0306e0-591a-5445-9a33-80f499426b38"
|
||||
"?project_id=awoooi"
|
||||
@@ -538,6 +602,7 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None:
|
||||
assert dumped["items"][0]["persisted_km_stale_completion_summary"]["triage"][
|
||||
"ai_lead_agent"
|
||||
] == "Hermes"
|
||||
assert dumped["items"][0]["evidence_capture_status"]["status"] == "captured"
|
||||
assert dumped["items"][0]["run_detail_href"].endswith("project_id=awoooi")
|
||||
|
||||
|
||||
|
||||
@@ -2800,6 +2800,29 @@
|
||||
"awooopSnapshotMcp": "MCP: total {total} / success {success} / failed {failed} / blocked {blocked}; top {topTool}",
|
||||
"awooopSnapshotExecution": "Execution: executor {executor}; playbook {playbook}; Ansible considered={ansible} / candidates={candidates}",
|
||||
"awooopSnapshotSource": "Source: {status}; direct {direct} / candidate {candidate} / applied {applied}; {providers}",
|
||||
"capture": {
|
||||
"title": "Evidence Capture Status",
|
||||
"captured": "Captured: {items}",
|
||||
"missing": "Missing: {items}",
|
||||
"nextAction": "Next action: {action}",
|
||||
"reason": "reason={reason}; rollout={rollout}",
|
||||
"none": "None",
|
||||
"statuses": {
|
||||
"captured": "Captured",
|
||||
"partial": "Partially captured",
|
||||
"not_captured": "Not captured",
|
||||
"observed": "Recorded"
|
||||
},
|
||||
"items": {
|
||||
"awooopStatusChain": "AwoooP status chain",
|
||||
"kmCompletionSummary": "KM owner-review snapshot"
|
||||
},
|
||||
"nextActions": {
|
||||
"none": "No follow-up needed",
|
||||
"press_telegram_detail_or_history_after_rollout": "Press Telegram Detail / History again to create a new callback snapshot",
|
||||
"observed": "Wait for the next callback evidence"
|
||||
}
|
||||
},
|
||||
"kmCompletion": {
|
||||
"title": "KM Owner Review",
|
||||
"status": "Status: {status}",
|
||||
|
||||
@@ -2801,6 +2801,29 @@
|
||||
"awooopSnapshotMcp": "MCP:total {total} / success {success} / failed {failed} / blocked {blocked};top {topTool}",
|
||||
"awooopSnapshotExecution": "Execution:executor {executor};playbook {playbook};Ansible considered={ansible} / candidates={candidates}",
|
||||
"awooopSnapshotSource": "Source:{status};direct {direct} / candidate {candidate} / applied {applied};{providers}",
|
||||
"capture": {
|
||||
"title": "Evidence Capture 狀態",
|
||||
"captured": "已捕捉:{items}",
|
||||
"missing": "尚缺:{items}",
|
||||
"nextAction": "下一步:{action}",
|
||||
"reason": "reason={reason};rollout={rollout}",
|
||||
"none": "無",
|
||||
"statuses": {
|
||||
"captured": "已捕捉",
|
||||
"partial": "部分捕捉",
|
||||
"not_captured": "未捕捉",
|
||||
"observed": "已記錄"
|
||||
},
|
||||
"items": {
|
||||
"awooopStatusChain": "AwoooP 狀態鏈",
|
||||
"kmCompletionSummary": "KM owner-review snapshot"
|
||||
},
|
||||
"nextActions": {
|
||||
"none": "不需補動作",
|
||||
"press_telegram_detail_or_history_after_rollout": "重新按 Telegram 詳情 / 歷史,產生新版 callback snapshot",
|
||||
"observed": "等待下一次 callback evidence"
|
||||
}
|
||||
},
|
||||
"kmCompletion": {
|
||||
"title": "KM Owner Review",
|
||||
"status": "狀態:{status}",
|
||||
|
||||
@@ -53,6 +53,11 @@ type CallbackReplyStatus =
|
||||
| "rescue_sent"
|
||||
| "failed"
|
||||
| "observed";
|
||||
type CallbackEvidenceCaptureState =
|
||||
| "captured"
|
||||
| "partial"
|
||||
| "not_captured"
|
||||
| "observed";
|
||||
type RemediationStatus =
|
||||
| "no_evidence"
|
||||
| "mcp_observed"
|
||||
@@ -114,6 +119,18 @@ interface CallbackReplySummary {
|
||||
latest_provider_message_id?: string | null;
|
||||
}
|
||||
|
||||
interface CallbackEvidenceCaptureStatus {
|
||||
schema_version?: string;
|
||||
status?: CallbackEvidenceCaptureState | string | null;
|
||||
reason?: string | null;
|
||||
action?: string | null;
|
||||
captured?: string[];
|
||||
missing?: string[];
|
||||
snapshot_rollout?: string | null;
|
||||
next_action?: string | null;
|
||||
event_at?: string | null;
|
||||
}
|
||||
|
||||
interface Run {
|
||||
run_id: string;
|
||||
project_id: string;
|
||||
@@ -373,6 +390,7 @@ interface CallbackReplyEvent {
|
||||
persisted_awooop_status_chain?: AwoooPStatusChain | null;
|
||||
km_stale_completion_summary?: KmStaleCompletionSummary | null;
|
||||
persisted_km_stale_completion_summary?: KmStaleCompletionSummary | null;
|
||||
evidence_capture_status?: CallbackEvidenceCaptureStatus | null;
|
||||
}
|
||||
|
||||
interface CallbackRepliesResponse {
|
||||
@@ -732,6 +750,19 @@ function normalizeCallbackReplyEventStatus(statusValue?: string | null): Callbac
|
||||
return "observed";
|
||||
}
|
||||
|
||||
function normalizeCallbackEvidenceCaptureStatus(
|
||||
statusValue?: string | null
|
||||
): CallbackEvidenceCaptureState {
|
||||
if (
|
||||
statusValue === "captured" ||
|
||||
statusValue === "partial" ||
|
||||
statusValue === "not_captured"
|
||||
) {
|
||||
return statusValue;
|
||||
}
|
||||
return "observed";
|
||||
}
|
||||
|
||||
function normalizeKmCompletionStatus(statusValue?: string | null) {
|
||||
if (
|
||||
statusValue === "matched_owner_review" ||
|
||||
@@ -744,6 +775,90 @@ function normalizeKmCompletionStatus(statusValue?: string | null) {
|
||||
return "observed";
|
||||
}
|
||||
|
||||
function CallbackEvidenceCaptureStatusPanel({
|
||||
captureStatus,
|
||||
}: {
|
||||
captureStatus?: CallbackEvidenceCaptureStatus | null;
|
||||
}) {
|
||||
const t = useTranslations("awooop.callbackReply.events.capture");
|
||||
if (!captureStatus) return null;
|
||||
|
||||
const state = normalizeCallbackEvidenceCaptureStatus(captureStatus.status);
|
||||
const config = {
|
||||
captured: {
|
||||
className: "border-[#8ec58e] bg-[#edf8ed] text-[#17602a]",
|
||||
icon: ShieldCheck,
|
||||
},
|
||||
partial: {
|
||||
className: "border-[#e1c36a] bg-[#fff7df] text-[#8a5a08]",
|
||||
icon: AlertCircle,
|
||||
},
|
||||
not_captured: {
|
||||
className: "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]",
|
||||
icon: BellOff,
|
||||
},
|
||||
observed: {
|
||||
className: "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]",
|
||||
icon: SearchCheck,
|
||||
},
|
||||
}[state];
|
||||
const Icon = config.icon;
|
||||
const itemLabels: Record<string, string> = {
|
||||
awooop_status_chain: t("items.awooopStatusChain"),
|
||||
km_stale_completion_summary: t("items.kmCompletionSummary"),
|
||||
};
|
||||
const formatItems = (items?: string[]) => (
|
||||
items && items.length > 0
|
||||
? items.map((item) => itemLabels[item] ?? item).join(" / ")
|
||||
: t("none")
|
||||
);
|
||||
const nextActionRaw = captureStatus.next_action ?? "observed";
|
||||
const nextActionKey = nextActionRaw === "none"
|
||||
|| nextActionRaw === "press_telegram_detail_or_history_after_rollout"
|
||||
? nextActionRaw
|
||||
: "observed";
|
||||
|
||||
return (
|
||||
<div className="mt-3 border-t border-[#e0ddd4] pt-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Icon className="h-3.5 w-3.5 text-[#1f5b9b]" aria-hidden="true" />
|
||||
<p className="text-xs font-semibold text-[#141413]">{t("title")}</p>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center border px-2 py-0.5 text-xs font-semibold",
|
||||
config.className
|
||||
)}
|
||||
>
|
||||
{t(`statuses.${state}` as never)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 grid gap-1 text-xs leading-5 text-[#5f5b52]">
|
||||
<p>
|
||||
{t("captured", {
|
||||
items: formatItems(captureStatus.captured),
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t("missing", {
|
||||
items: formatItems(captureStatus.missing),
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t("nextAction", {
|
||||
action: t(`nextActions.${nextActionKey}` as never),
|
||||
})}
|
||||
</p>
|
||||
<p className="truncate font-mono text-[#77736a]">
|
||||
{t("reason", {
|
||||
reason: captureStatus.reason ?? "--",
|
||||
rollout: captureStatus.snapshot_rollout ?? "--",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RemediationEvidenceCell({ summary }: { summary?: RemediationSummary | null }) {
|
||||
const t = useTranslations("awooop.listEvidence");
|
||||
const status = normalizeRemediationStatus(summary);
|
||||
@@ -1796,6 +1911,9 @@ function CallbackReplyEvidencePanel({
|
||||
compact
|
||||
className="mt-3"
|
||||
/>
|
||||
<CallbackEvidenceCaptureStatusPanel
|
||||
captureStatus={event.evidence_capture_status}
|
||||
/>
|
||||
<CallbackAwoooPStatusChainSnapshot
|
||||
chain={event.persisted_awooop_status_chain}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user