feat(awooop): surface callback replies on run list
All checks were successful
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m25s
CD Pipeline / build-and-deploy (push) Successful in 3m35s
CD Pipeline / post-deploy-checks (push) Successful in 1m50s

This commit is contained in:
Your Name
2026-05-18 15:24:39 +08:00
parent 584bd4b31b
commit 20d62ee0cf
5 changed files with 356 additions and 4 deletions

View File

@@ -217,6 +217,9 @@ async def list_runs(
"created_at": r.created_at,
"timeout_at": r.timeout_at,
"remediation_summary": remediation_summaries.get(r.run_id),
"callback_reply_summary": _run_callback_reply_summary(
outbound_by_run.get(r.run_id, [])
),
}
for r in rows
]
@@ -382,6 +385,69 @@ def _outbound_timeline_metadata(
return metadata
def _run_callback_reply_summary(
outbound_messages: list[AwoooPOutboundMessage],
) -> dict[str, Any]:
"""Summarize Telegram detail/history callback reply delivery for Run List."""
callback_rows: list[tuple[AwoooPOutboundMessage, dict[str, Any]]] = []
for row in outbound_messages:
callback_reply = _outbound_callback_reply(row.source_envelope)
if callback_reply:
callback_rows.append((row, callback_reply))
if not callback_rows:
return {
"schema_version": "awooop_run_callback_reply_summary_v1",
"status": "no_callback",
"total": 0,
"sent": 0,
"fallback_sent": 0,
"rescue_sent": 0,
"failed": 0,
"needs_human": False,
"latest_status": None,
"latest_action": None,
"latest_incident_id": None,
"latest_at": None,
"latest_provider_message_id": None,
}
sorted_rows = sorted(
callback_rows,
key=lambda item: str(item[0].sent_at or item[0].queued_at or ""),
reverse=True,
)
latest_row, latest_callback = sorted_rows[0]
statuses = [
str(callback.get("status") or "")
for _, callback in sorted_rows
]
failed = statuses.count("callback_reply_failed")
latest_status = str(latest_callback.get("status") or "")
summary_status = {
"callback_reply_sent": "sent",
"callback_reply_fallback_sent": "fallback_sent",
"callback_reply_rescue_sent": "rescue_sent",
"callback_reply_failed": "failed",
}.get(latest_status, "observed")
return {
"schema_version": "awooop_run_callback_reply_summary_v1",
"status": summary_status,
"total": len(sorted_rows),
"sent": statuses.count("callback_reply_sent"),
"fallback_sent": statuses.count("callback_reply_fallback_sent"),
"rescue_sent": statuses.count("callback_reply_rescue_sent"),
"failed": failed,
"needs_human": failed > 0 or latest_status == "callback_reply_failed",
"latest_status": latest_status or None,
"latest_action": latest_callback.get("action"),
"latest_incident_id": latest_callback.get("incident_id"),
"latest_at": latest_row.sent_at or latest_row.queued_at,
"latest_provider_message_id": latest_row.provider_message_id,
}
def _mcp_gateway_summary_row(row: AwoooPMcpGatewayAudit) -> dict[str, Any]:
"""Convert SQLAlchemy audit rows into the truth-chain summary shape."""
return {
@@ -441,6 +507,7 @@ def _collect_run_incident_ids(
_append_incident_ids_from_text(incident_ids, event.content_redacted)
for message in outbound_messages:
_append_incident_ids_from_source_envelope(incident_ids, message.source_envelope)
_append_incident_ids_from_text(incident_ids, message.content_preview)
_append_incident_ids_from_text(incident_ids, message.send_error)

View File

@@ -12,6 +12,7 @@ from src.services.platform_operator_service import (
_remediation_summary_matches_incident_id,
_remediation_summary_matches_status,
_remediation_timeline_summary,
_run_callback_reply_summary,
_run_remediation_list_summary,
_timeline_sort_key,
)
@@ -129,6 +130,11 @@ def test_collect_run_incident_ids_reads_source_refs_and_legacy_text() -> None:
]
outbound_messages = [
SimpleNamespace(
source_envelope={
"source_refs": {
"incident_ids": ["INC-20260518-CB0001"],
},
},
content_preview="詳情INC-20260513-79ED5E",
send_error=None,
)
@@ -140,7 +146,84 @@ def test_collect_run_incident_ids_reads_source_refs_and_legacy_text() -> None:
outbound_messages=outbound_messages,
)
assert incident_ids == ["INC-20260514-F85F21", "INC-20260513-79ED5E"]
assert incident_ids == [
"INC-20260514-F85F21",
"INC-20260518-CB0001",
"INC-20260513-79ED5E",
]
def test_run_callback_reply_summary_marks_latest_fallback() -> None:
summary = _run_callback_reply_summary([
SimpleNamespace(
source_envelope={
"callback_reply": {
"status": "callback_reply_sent",
"action": "detail",
"incident_id": "INC-20260513-79ED5E",
}
},
sent_at=datetime(2026, 5, 18, 6, 1, 0),
queued_at=datetime(2026, 5, 18, 6, 1, 0),
provider_message_id="100",
),
SimpleNamespace(
source_envelope={
"callback_reply": {
"status": "callback_reply_fallback_sent",
"action": "history",
"incident_id": "INC-20260513-79ED5E",
}
},
sent_at=datetime(2026, 5, 18, 6, 2, 0),
queued_at=datetime(2026, 5, 18, 6, 2, 0),
provider_message_id="101",
),
])
assert summary["status"] == "fallback_sent"
assert summary["total"] == 2
assert summary["sent"] == 1
assert summary["fallback_sent"] == 1
assert summary["latest_action"] == "history"
assert summary["latest_incident_id"] == "INC-20260513-79ED5E"
assert summary["latest_provider_message_id"] == "101"
assert summary["needs_human"] is False
def test_run_callback_reply_summary_marks_failed_as_human_attention() -> None:
summary = _run_callback_reply_summary([
SimpleNamespace(
source_envelope={
"callback_reply": {
"status": "callback_reply_failed",
"action": "detail",
"incident_id": "INC-20260513-79ED5E",
}
},
sent_at=None,
queued_at=datetime(2026, 5, 18, 6, 3, 0),
provider_message_id="telegram_callback_reply:failed",
)
])
assert summary["status"] == "failed"
assert summary["failed"] == 1
assert summary["needs_human"] is True
def test_run_callback_reply_summary_marks_no_callback() -> None:
summary = _run_callback_reply_summary([
SimpleNamespace(
source_envelope={},
sent_at=datetime(2026, 5, 18, 6, 1, 0),
queued_at=datetime(2026, 5, 18, 6, 1, 0),
provider_message_id="100",
)
])
assert summary["status"] == "no_callback"
assert summary["total"] == 0
def test_remediation_timeline_summary_surfaces_route_and_write_flags() -> None: