From 81588f9a44d4b1d4c2d386f572590e75de6bd85a Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 29 Jun 2026 17:38:06 +0800 Subject: [PATCH] feat(awooop): expose alert noise reduction readback --- .../ai_agent_autonomous_runtime_control.py | 410 +++++++++++++++++- ...est_ai_agent_autonomous_runtime_control.py | 53 ++- apps/web/messages/en.json | 1 + apps/web/messages/zh-TW.json | 1 + .../autonomous-runtime-receipt-panel.tsx | 43 ++ 5 files changed, 503 insertions(+), 5 deletions(-) diff --git a/apps/api/src/services/ai_agent_autonomous_runtime_control.py b/apps/api/src/services/ai_agent_autonomous_runtime_control.py index 2f6ee0204..953befb12 100644 --- a/apps/api/src/services/ai_agent_autonomous_runtime_control.py +++ b/apps/api/src/services/ai_agent_autonomous_runtime_control.py @@ -136,22 +136,48 @@ def _status_counts( status_key: str, ) -> dict[str, Any]: by_status: dict[str, int] = {} + recent_by_status: dict[str, int] = {} total = 0 recent = 0 for row in rows: item = _row_mapping(row) status = str(item.get(status_key) or "unknown") row_total = _int_value(item.get("total")) + row_recent = _int_value(item.get("recent")) by_status[status] = by_status.get(status, 0) + row_total + recent_by_status[status] = recent_by_status.get(status, 0) + row_recent total += row_total - recent += _int_value(item.get("recent")) + recent += row_recent return { "total": total, "recent": recent, "by_status": by_status, + "recent_by_status": recent_by_status, } +def _status_total(summary: Mapping[str, Any] | None, *statuses: str) -> int: + if not isinstance(summary, Mapping): + return 0 + if not statuses: + return _int_value(summary.get("total")) + by_status = summary.get("by_status") + if not isinstance(by_status, Mapping): + return 0 + return sum(_int_value(by_status.get(status)) for status in statuses) + + +def _status_recent(summary: Mapping[str, Any] | None, *statuses: str) -> int: + if not isinstance(summary, Mapping): + return 0 + if not statuses: + return _int_value(summary.get("recent")) + by_status = summary.get("recent_by_status") + if not isinstance(by_status, Mapping): + return 0 + return sum(_int_value(by_status.get(status)) for status in statuses) + + def _trace_stage( *, stage_id: str, @@ -976,12 +1002,253 @@ def _build_learning_loop_readback( } +def _alert_noise_stage( + *, + stage_id: str, + display_name: str, + evidence_sources: list[str], + total: int, + recent: int, + required_for_noise_reduction: bool, + feeds_controlled_queue: bool, + next_action_if_missing: str, +) -> dict[str, Any]: + present = total > 0 + return { + "stage_id": stage_id, + "display_name": display_name, + "evidence_sources": evidence_sources, + "present": present, + "total": max(0, total), + "recent": max(0, recent), + "required_for_noise_reduction": required_for_noise_reduction, + "feeds_controlled_queue": feeds_controlled_queue, + "next_action_if_missing": None if present else next_action_if_missing, + } + + +def _build_alert_noise_reduction_readback( + *, + alert_operation_summary: Mapping[str, Any], + alertmanager_event_summary: Mapping[str, Any], + grouped_alert_summary: Mapping[str, Any], + operation_summary: Mapping[str, Any], + agent_decision_wiring: Mapping[str, Any], + learning_loop: Mapping[str, Any], +) -> dict[str, Any]: + """Expose alert storm control and AI controlled routing receipts.""" + + alert_received_total = ( + _status_total(alert_operation_summary, "ALERT_RECEIVED") + + _status_total(alertmanager_event_summary, "received") + ) + alert_received_recent = ( + _status_recent(alert_operation_summary, "ALERT_RECEIVED") + + _status_recent(alertmanager_event_summary, "received") + ) + converged_duplicate_total = _status_total(alertmanager_event_summary, "converged") + converged_duplicate_recent = _status_recent(alertmanager_event_summary, "converged") + llm_inflight_suppressed_total = _status_total( + alertmanager_event_summary, + "llm_inflight_suppressed", + ) + llm_inflight_suppressed_recent = _status_recent( + alertmanager_event_summary, + "llm_inflight_suppressed", + ) + grouped_child_total = _status_total(grouped_alert_summary, "grouped_child_alert") + grouped_child_recent = _status_recent(grouped_alert_summary, "grouped_child_alert") + duplicate_convergence_total = ( + converged_duplicate_total + llm_inflight_suppressed_total + grouped_child_total + ) + duplicate_convergence_recent = ( + converged_duplicate_recent + llm_inflight_suppressed_recent + grouped_child_recent + ) + controlled_route_total = ( + _trace_total( + operation_summary, + "ansible_candidate_matched", + "ansible_check_mode_executed", + "ansible_apply_executed", + ) + + _status_total( + alert_operation_summary, + "AUTO_REPAIR_TRIGGERED", + "EXECUTION_STARTED", + "EXECUTION_COMPLETED", + "NOTIFICATION_CLASSIFIED", + ) + ) + controlled_route_recent = ( + _trace_recent( + operation_summary, + "ansible_candidate_matched", + "ansible_check_mode_executed", + "ansible_apply_executed", + ) + + _status_recent( + alert_operation_summary, + "AUTO_REPAIR_TRIGGERED", + "EXECUTION_STARTED", + "EXECUTION_COMPLETED", + "NOTIFICATION_CLASSIFIED", + ) + ) + guardrail_total = _status_total( + alert_operation_summary, + "GUARDRAIL_BLOCKED", + "STATE_GUARD_BLOCKED", + "ESCALATED", + "SILENCED", + ) + guardrail_recent = _status_recent( + alert_operation_summary, + "GUARDRAIL_BLOCKED", + "STATE_GUARD_BLOCKED", + "ESCALATED", + "SILENCED", + ) + decision_complete = agent_decision_wiring.get("status") == "completed" + learning_complete = learning_loop.get("status") == "completed" + + stages = [ + _alert_noise_stage( + stage_id="alert_intake_receipts", + display_name="Alertmanager receipts recorded", + evidence_sources=[ + "alert_operation_log:ALERT_RECEIVED", + "awooop_conversation_event:received", + ], + total=alert_received_total, + recent=alert_received_recent, + required_for_noise_reduction=True, + feeds_controlled_queue=True, + next_action_if_missing="record_alertmanager_received_events_before_any_notification_or_ai_route", + ), + _alert_noise_stage( + stage_id="duplicate_convergence", + display_name="Duplicate and recurring alerts converge", + evidence_sources=[ + "awooop_conversation_event:converged", + "awooop_conversation_event:llm_inflight_suppressed", + "awooop_conversation_event:alert-group", + ], + total=duplicate_convergence_total, + recent=duplicate_convergence_recent, + required_for_noise_reduction=True, + feeds_controlled_queue=True, + next_action_if_missing="enable_converged_fingerprint_and_grouped_child_alert_receipts", + ), + _alert_noise_stage( + stage_id="notification_suppression", + display_name="Telegram flood is suppressed into parent/digest receipts", + evidence_sources=[ + "awooop_conversation_event:alert-group", + "telegram_gateway:grouped_alert_digest_dedup", + ], + total=grouped_child_total + llm_inflight_suppressed_total, + recent=grouped_child_recent + llm_inflight_suppressed_recent, + required_for_noise_reduction=True, + feeds_controlled_queue=False, + next_action_if_missing="write_grouped_child_alert_event_or_inflight_suppression_receipt", + ), + _alert_noise_stage( + stage_id="ai_controlled_routing", + display_name="Alerts route to AI controlled candidate/check/apply queue", + evidence_sources=[ + "automation_operation_log:ansible_candidate_matched", + "automation_operation_log:ansible_check_mode_executed", + "automation_operation_log:ansible_apply_executed", + "alert_operation_log:AUTO_REPAIR_TRIGGERED", + ], + total=controlled_route_total if decision_complete else 0, + recent=controlled_route_recent, + required_for_noise_reduction=True, + feeds_controlled_queue=True, + next_action_if_missing="route_repeated_non_critical_alerts_to_controlled_candidate_check_apply", + ), + _alert_noise_stage( + stage_id="learning_feedback", + display_name="Suppressed alert patterns feed KM/RAG/PlayBook learning", + evidence_sources=[ + "alert_noise_reduction", + "ai_agent_learning_loop_readback", + ], + total=1 if learning_complete and duplicate_convergence_total > 0 else 0, + recent=1 if learning_complete and duplicate_convergence_recent > 0 else 0, + required_for_noise_reduction=True, + feeds_controlled_queue=True, + next_action_if_missing="keep_p1c_learning_loop_complete_before_closing_alert_noise_reduction", + ), + _alert_noise_stage( + stage_id="break_glass_boundary", + display_name="Critical / guardrail cases remain isolated from default alert routing", + evidence_sources=[ + "alert_operation_log:GUARDRAIL_BLOCKED", + "alert_operation_log:ESCALATED", + "current_policy:critical_break_glass_required", + ], + total=guardrail_total, + recent=guardrail_recent, + required_for_noise_reduction=False, + feeds_controlled_queue=False, + next_action_if_missing="record_guardrail_or_break_glass_receipts_only_for_true_hard_blockers", + ), + ] + missing_required = [ + str(stage["stage_id"]) + for stage in stages + if stage["required_for_noise_reduction"] is True and stage["present"] is not True + ] + present_required_count = sum( + 1 + for stage in stages + if stage["required_for_noise_reduction"] is True and stage["present"] is True + ) + required_count = sum(1 for stage in stages if stage["required_for_noise_reduction"] is True) + return { + "schema_version": "ai_agent_alert_noise_reduction_readback_v1", + "status": "completed" if not missing_required else "in_progress", + "stages": stages, + "missing_required_stage_ids": missing_required, + "routing_policy": { + "manual_default_route_allowed": False, + "low_medium_high_alerts_route_to_ai_controlled_queue": True, + "critical_break_glass_still_required": True, + "telegram_child_alert_flood_allowed": False, + }, + "public_safety": { + "stores_raw_alert_payload": False, + "stores_secret_values": False, + "executes_on_read": False, + "reads_raw_sessions": False, + }, + "rollups": { + "stage_count": len(stages), + "required_stage_count": required_count, + "required_stage_present_count": present_required_count, + "required_stage_missing_count": len(missing_required), + "alert_received_total": alert_received_total, + "alert_received_recent": alert_received_recent, + "converged_duplicate_total": converged_duplicate_total, + "llm_inflight_suppressed_total": llm_inflight_suppressed_total, + "grouped_child_alert_total": grouped_child_total, + "suppressed_alert_total": duplicate_convergence_total, + "suppressed_alert_recent": duplicate_convergence_recent, + "controlled_route_total": controlled_route_total, + "controlled_route_recent": controlled_route_recent, + "break_glass_or_guardrail_total": guardrail_total, + }, + } + + def _build_work_item_progress( *, trace_ledger: Mapping[str, Any], log_integration_taxonomy: Mapping[str, Any], agent_decision_wiring: Mapping[str, Any], learning_loop: Mapping[str, Any], + alert_noise_reduction: Mapping[str, Any], db_read_status: str, ) -> dict[str, Any]: """Build ordered work items that the UI and agent can keep advancing.""" @@ -1015,6 +1282,16 @@ def _build_work_item_progress( and learning_loop.get("schema_version") == "ai_agent_learning_loop_readback_v1" and learning_loop_missing == 0 ) + alert_noise_rollups = alert_noise_reduction.get("rollups") + if not isinstance(alert_noise_rollups, Mapping): + alert_noise_rollups = {} + alert_noise_missing = _int_value(alert_noise_rollups.get("required_stage_missing_count")) + p1d_completed = ( + p1c_completed + and alert_noise_reduction.get("schema_version") + == "ai_agent_alert_noise_reduction_readback_v1" + and alert_noise_missing == 0 + ) deployed_readback_complete = ( db_read_status == "ok" and trace_ledger.get("schema_version") == "ai_agent_autonomous_trace_ledger_v1" @@ -1086,8 +1363,9 @@ def _build_work_item_progress( "work_item_id": "P1-D-alert-noise-reduction", "priority": "P1-D", "title": "Alert grouping and AI controlled workflow routing", - "status": "pending", + "status": "completed" if p1d_completed else "in_progress" if p1c_completed else "pending", "exit_criteria": "repeated alerts are clustered, deduped, routed to controlled automation, and no longer default to manual handling", + "remaining_alert_noise_stage_count": alert_noise_missing, }, { "work_item_id": "P2-A-ui-ux-productization", @@ -1869,6 +2147,9 @@ def build_runtime_receipt_readback_from_rows( executor_log_count_rows: Iterable[Mapping[str, Any] | Any] = (), timeline_count_rows: Iterable[Mapping[str, Any] | Any] = (), playbook_trust_count_rows: Iterable[Mapping[str, Any] | Any] = (), + alert_operation_count_rows: Iterable[Mapping[str, Any] | Any] = (), + alertmanager_event_count_rows: Iterable[Mapping[str, Any] | Any] = (), + grouped_alert_event_count_rows: Iterable[Mapping[str, Any] | Any] = (), error_type: str | None = None, ) -> dict[str, Any]: """Build the live executor receipt readback from already-fetched rows.""" @@ -1895,6 +2176,18 @@ def build_runtime_receipt_readback_from_rows( executor_log_summary = _status_counts(executor_log_count_rows, status_key="status") timeline_summary = _status_counts(timeline_count_rows, status_key="status") playbook_trust_summary = _status_counts(playbook_trust_count_rows, status_key="status") + alert_operation_summary = _status_counts( + alert_operation_count_rows, + status_key="event_type", + ) + alertmanager_event_summary = _status_counts( + alertmanager_event_count_rows, + status_key="stage", + ) + grouped_alert_summary = _status_counts( + grouped_alert_event_count_rows, + status_key="status", + ) latest_closure = _latest_flow_closure( operation_latest_rows=operation_latest, verifier_latest_rows=verifier_latest, @@ -1971,11 +2264,20 @@ def build_runtime_receipt_readback_from_rows( controlled_retry_package=retry_package, loop_ledger=loop_ledger, ) + alert_noise_reduction = _build_alert_noise_reduction_readback( + alert_operation_summary=alert_operation_summary, + alertmanager_event_summary=alertmanager_event_summary, + grouped_alert_summary=grouped_alert_summary, + operation_summary=operation_summary, + agent_decision_wiring=agent_decision_wiring, + learning_loop=learning_loop, + ) work_item_progress = _build_work_item_progress( trace_ledger=trace_ledger, log_integration_taxonomy=log_integration_taxonomy, agent_decision_wiring=agent_decision_wiring, learning_loop=learning_loop, + alert_noise_reduction=alert_noise_reduction, db_read_status=db_read_status, ) apply_summary = operation_summary.get("ansible_apply_executed") or {} @@ -2099,6 +2401,7 @@ def build_runtime_receipt_readback_from_rows( "log_integration_taxonomy": log_integration_taxonomy, "agent_decision_wiring": agent_decision_wiring, "learning_loop": learning_loop, + "alert_noise_reduction": alert_noise_reduction, "work_item_progress": work_item_progress, } if error_type: @@ -2247,6 +2550,36 @@ def _attach_runtime_receipt_readback( "similar_case_source_total" ) ), + "live_alert_noise_stage_count": _int_value( + ((readback.get("alert_noise_reduction") or {}).get("rollups") or {}).get( + "stage_count" + ) + ), + "live_alert_noise_required_present_count": _int_value( + ((readback.get("alert_noise_reduction") or {}).get("rollups") or {}).get( + "required_stage_present_count" + ) + ), + "live_alert_noise_required_missing_count": _int_value( + ((readback.get("alert_noise_reduction") or {}).get("rollups") or {}).get( + "required_stage_missing_count" + ) + ), + "live_alert_noise_complete_count": ( + 1 + if (readback.get("alert_noise_reduction") or {}).get("status") == "completed" + else 0 + ), + "live_alert_noise_suppressed_count": _int_value( + ((readback.get("alert_noise_reduction") or {}).get("rollups") or {}).get( + "suppressed_alert_total" + ) + ), + "live_alert_noise_controlled_route_count": _int_value( + ((readback.get("alert_noise_reduction") or {}).get("rollups") or {}).get( + "controlled_route_total" + ) + ), "live_work_item_count": _int_value( ((readback.get("work_item_progress") or {}).get("rollups") or {}).get( "work_item_count" @@ -2387,7 +2720,7 @@ def build_ai_agent_autonomous_runtime_control() -> dict[str, Any]: "deploy_readback_marker": _DEPLOY_READBACK_MARKER, "deploy_attempt_note": _DEPLOY_ATTEMPT_NOTE, "legacy_no_send_no_live_rules_overridden": True, - "implementation_completion_percent": 88, + "implementation_completion_percent": 91, "status_note": ( "目前有效規則:low / medium / high 風險由 AI Agent 在 allowlist、" "Ansible check-mode、verifier、rollback、KM 與 Telegram receipt 下受控自動處理。" @@ -2600,6 +2933,18 @@ async def load_ai_agent_autonomous_runtime_receipt_readback( _RUNTIME_PLAYBOOK_TRUST_COUNTS_SQL, _RUNTIME_PLAYBOOK_TRUST_COUNTS_FALLBACK_SQL, ) + alert_operation_counts = await _safe_aux_rows( + "alert_operation_counts", + _RUNTIME_ALERT_OPERATION_COUNTS_SQL, + ) + alertmanager_event_counts = await _safe_aux_rows( + "alertmanager_event_counts", + _RUNTIME_ALERTMANAGER_EVENT_COUNTS_SQL, + ) + grouped_alert_event_counts = await _safe_aux_rows( + "grouped_alert_event_counts", + _RUNTIME_GROUPED_ALERT_EVENT_COUNTS_SQL, + ) except Exception as exc: logger.warning( "ai_agent_autonomous_runtime_receipt_readback_failed", @@ -2633,6 +2978,9 @@ async def load_ai_agent_autonomous_runtime_receipt_readback( executor_log_count_rows=executor_log_counts, timeline_count_rows=timeline_counts, playbook_trust_count_rows=playbook_trust_counts, + alert_operation_count_rows=alert_operation_counts, + alertmanager_event_count_rows=alertmanager_event_counts, + grouped_alert_event_count_rows=grouped_alert_event_counts, ) @@ -2984,6 +3332,62 @@ _RUNTIME_PLAYBOOK_TRUST_COUNTS_FALLBACK_SQL = """ """ +_RUNTIME_ALERT_OPERATION_COUNTS_SQL = """ + SELECT + event_type, + count(*) AS total, + count(*) FILTER ( + WHERE created_at >= NOW() - (:lookback_hours * INTERVAL '1 hour') + ) AS recent + FROM alert_operation_log + WHERE event_type IN ( + 'ALERT_RECEIVED', + 'AUTO_REPAIR_TRIGGERED', + 'EXECUTION_STARTED', + 'EXECUTION_COMPLETED', + 'NOTIFICATION_CLASSIFIED', + 'GUARDRAIL_BLOCKED', + 'STATE_GUARD_BLOCKED', + 'SILENCED', + 'ESCALATED' + ) + GROUP BY event_type + ORDER BY event_type +""" + + +_RUNTIME_ALERTMANAGER_EVENT_COUNTS_SQL = """ + SELECT + COALESCE(NULLIF(source_envelope ->> 'stage', ''), 'unknown') AS stage, + count(*) AS total, + count(*) FILTER ( + WHERE received_at >= NOW() - (:lookback_hours * INTERVAL '1 hour') + ) AS recent + FROM awooop_conversation_event + WHERE project_id = :project_id + AND COALESCE( + NULLIF(source_envelope ->> 'provider', ''), + platform_subject_id, + '' + ) = 'alertmanager' + GROUP BY COALESCE(NULLIF(source_envelope ->> 'stage', ''), 'unknown') + ORDER BY stage +""" + + +_RUNTIME_GROUPED_ALERT_EVENT_COUNTS_SQL = """ + SELECT + 'grouped_child_alert' AS status, + count(*) AS total, + count(*) FILTER ( + WHERE received_at >= NOW() - (:lookback_hours * INTERVAL '1 hour') + ) AS recent + FROM awooop_conversation_event + WHERE project_id = :project_id + AND channel_chat_id LIKE 'alert-group:%' +""" + + def _validate_payload(payload: dict[str, Any]) -> None: if payload.get("schema_version") != _SCHEMA_VERSION: raise ValueError(f"schema_version must be {_SCHEMA_VERSION}") diff --git a/apps/api/tests/test_ai_agent_autonomous_runtime_control.py b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py index 86dbabfeb..027ee0c2a 100644 --- a/apps/api/tests/test_ai_agent_autonomous_runtime_control.py +++ b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py @@ -1,4 +1,7 @@ from src.services.ai_agent_autonomous_runtime_control import ( + _RUNTIME_ALERTMANAGER_EVENT_COUNTS_SQL, + _RUNTIME_ALERT_OPERATION_COUNTS_SQL, + _RUNTIME_GROUPED_ALERT_EVENT_COUNTS_SQL, _RUNTIME_PLAYBOOK_TRUST_COUNTS_FALLBACK_SQL, _RUNTIME_TIMELINE_COUNTS_SQL, build_ai_agent_autonomous_runtime_control, @@ -14,6 +17,11 @@ def test_runtime_receipt_auxiliary_sql_keeps_source_family_counts_schema_safe(): assert "FROM playbooks" in _RUNTIME_PLAYBOOK_TRUST_COUNTS_FALLBACK_SQL assert "updated_at" not in _RUNTIME_PLAYBOOK_TRUST_COUNTS_FALLBACK_SQL assert "trust_score" not in _RUNTIME_PLAYBOOK_TRUST_COUNTS_FALLBACK_SQL + assert "FROM alert_operation_log" in _RUNTIME_ALERT_OPERATION_COUNTS_SQL + assert "ALERT_RECEIVED" in _RUNTIME_ALERT_OPERATION_COUNTS_SQL + assert "FROM awooop_conversation_event" in _RUNTIME_ALERTMANAGER_EVENT_COUNTS_SQL + assert "source_envelope ->> 'stage'" in _RUNTIME_ALERTMANAGER_EVENT_COUNTS_SQL + assert "channel_chat_id LIKE 'alert-group:%'" in _RUNTIME_GROUPED_ALERT_EVENT_COUNTS_SQL def test_ai_agent_autonomous_runtime_control_uses_current_owner_directive(): @@ -30,7 +38,7 @@ def test_ai_agent_autonomous_runtime_control_uses_current_owner_directive(): "cd_internal_control_plane_readback_retry_20260628_2" ) assert data["program_status"]["legacy_no_send_no_live_rules_overridden"] is True - assert data["program_status"]["implementation_completion_percent"] == 88 + assert data["program_status"]["implementation_completion_percent"] == 91 assert data["current_policy"]["low_risk_controlled_apply_allowed"] is True assert data["current_policy"]["medium_risk_controlled_apply_allowed"] is True assert data["current_policy"]["high_risk_controlled_apply_allowed"] is True @@ -303,6 +311,22 @@ def test_runtime_receipt_readback_summarizes_live_executor_closure_rows(): playbook_trust_count_rows=[ {"status": "learning_active", "total": 4, "recent": 1}, ], + alert_operation_count_rows=[ + {"event_type": "ALERT_RECEIVED", "total": 5, "recent": 2}, + {"event_type": "AUTO_REPAIR_TRIGGERED", "total": 2, "recent": 1}, + {"event_type": "EXECUTION_STARTED", "total": 1, "recent": 1}, + {"event_type": "EXECUTION_COMPLETED", "total": 1, "recent": 1}, + {"event_type": "NOTIFICATION_CLASSIFIED", "total": 1, "recent": 1}, + {"event_type": "GUARDRAIL_BLOCKED", "total": 1, "recent": 0}, + ], + alertmanager_event_count_rows=[ + {"stage": "received", "total": 5, "recent": 2}, + {"stage": "converged", "total": 3, "recent": 1}, + {"stage": "llm_inflight_suppressed", "total": 1, "recent": 0}, + ], + grouped_alert_event_count_rows=[ + {"status": "grouped_child_alert", "total": 4, "recent": 1}, + ], ) assert readback["db_read_status"] == "ok" @@ -466,6 +490,29 @@ def test_runtime_receipt_readback_summarizes_live_executor_closure_rows(): assert learning_loop["rollups"]["repair_feedback_ready_count"] == 1 assert learning_loop["public_safety"]["stores_secret_values"] is False assert learning_loop["public_safety"]["executes_on_read"] is False + alert_noise = readback["alert_noise_reduction"] + assert alert_noise["schema_version"] == "ai_agent_alert_noise_reduction_readback_v1" + assert alert_noise["status"] == "completed" + assert alert_noise["missing_required_stage_ids"] == [] + assert { + stage["stage_id"] + for stage in alert_noise["stages"] + if stage["required_for_noise_reduction"] + } == { + "alert_intake_receipts", + "duplicate_convergence", + "notification_suppression", + "ai_controlled_routing", + "learning_feedback", + } + assert alert_noise["rollups"]["required_stage_present_count"] == 5 + assert alert_noise["rollups"]["required_stage_missing_count"] == 0 + assert alert_noise["rollups"]["alert_received_total"] == 10 + assert alert_noise["rollups"]["suppressed_alert_total"] == 8 + assert alert_noise["rollups"]["controlled_route_total"] == 8 + assert alert_noise["routing_policy"]["manual_default_route_allowed"] is False + assert alert_noise["routing_policy"]["low_medium_high_alerts_route_to_ai_controlled_queue"] is True + assert alert_noise["public_safety"]["stores_raw_alert_payload"] is False progress = readback["work_item_progress"] assert progress["schema_version"] == "ai_agent_automation_work_item_progress_v1" ordered_ids = [item["work_item_id"] for item in progress["ordered_items"]] @@ -487,10 +534,12 @@ def test_runtime_receipt_readback_summarizes_live_executor_closure_rows(): assert progress["ordered_items"][6]["status"] == "completed" assert progress["ordered_items"][7]["status"] == "completed" assert progress["ordered_items"][7]["remaining_learning_loop_stage_count"] == 0 + assert progress["ordered_items"][8]["status"] == "completed" + assert progress["ordered_items"][8]["remaining_alert_noise_stage_count"] == 0 assert progress["source_family_items"] assert {item["status"] for item in progress["source_family_items"]} == {"completed"} assert progress["rollups"]["source_family_work_item_count"] == 10 - assert progress["rollups"]["pending_count"] >= 3 + assert progress["rollups"]["pending_count"] >= 2 def test_runtime_receipt_readback_classifies_closed_failed_apply_as_ai_repair(): diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 78c6af4d6..f00038265 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -11347,6 +11347,7 @@ "logs": "Logs", "decision": "Decision", "learning": "Learning", + "alerts": "Alerts", "apply": "Apply", "receipt": "Receipt", "verifier": "Verifier", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index c08634c53..8d7ba06f9 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -11347,6 +11347,7 @@ "logs": "Logs", "decision": "Decision", "learning": "Learning", + "alerts": "Alerts", "apply": "Apply", "receipt": "Receipt", "verifier": "Verifier", diff --git a/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx b/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx index f8f46abdc..909bc756b 100644 --- a/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx +++ b/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useLocale, useTranslations } from "next-intl"; import { Activity, + Bell, Bot, BookOpenCheck, CheckCircle2, @@ -107,6 +108,17 @@ type RuntimeReceiptReadback = { similar_case_source_total?: number | null; } | null; } | null; + alert_noise_reduction?: { + status?: string | null; + missing_required_stage_ids?: string[] | null; + rollups?: { + stage_count?: number | null; + required_stage_present_count?: number | null; + required_stage_missing_count?: number | null; + suppressed_alert_total?: number | null; + controlled_route_total?: number | null; + } | null; + } | null; work_item_progress?: { rollups?: { work_item_count?: number | null; @@ -240,6 +252,8 @@ export function AutonomousRuntimeReceiptPanel({ const decisionMissing = readback?.agent_decision_wiring?.missing_required_stage_ids ?? []; const learningRollups = readback?.learning_loop?.rollups ?? {}; const learningMissing = readback?.learning_loop?.missing_required_stage_ids ?? []; + const alertNoiseRollups = readback?.alert_noise_reduction?.rollups ?? {}; + const alertNoiseMissing = readback?.alert_noise_reduction?.missing_required_stage_ids ?? []; const workItemRollups = readback?.work_item_progress?.rollups ?? {}; const latestFlow = readback?.latest_flow_closure; const rollups = payload?.rollups ?? {}; @@ -343,6 +357,31 @@ export function AutonomousRuntimeReceiptPanel({ ), }), }, + { + key: "alerts", + label: t("metrics.alerts"), + value: toNumber( + rollups.live_alert_noise_required_present_count + ?? alertNoiseRollups.required_stage_present_count + ), + recent: toNumber( + rollups.live_alert_noise_stage_count + ?? alertNoiseRollups.stage_count + ), + icon: Bell, + caption: t("traceCaption", { + count: numberValue( + rollups.live_alert_noise_stage_count + ?? alertNoiseRollups.stage_count + ?? 0 + ), + missing: numberValue( + rollups.live_alert_noise_required_missing_count + ?? alertNoiseRollups.required_stage_missing_count + ?? alertNoiseMissing.length + ), + }), + }, { key: "apply", label: t("metrics.apply"), @@ -392,6 +431,10 @@ export function AutonomousRuntimeReceiptPanel({ decisionRollups.required_stage_present_count, decisionRollups.stage_count, learningMissing.length, + alertNoiseMissing.length, + alertNoiseRollups.required_stage_missing_count, + alertNoiseRollups.required_stage_present_count, + alertNoiseRollups.stage_count, learningRollups.required_stage_missing_count, learningRollups.required_stage_present_count, learningRollups.stage_count,