From 32d0109dcdbe2b5587ecf34e4fc0db14eacbf644 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 2 Jul 2026 20:20:48 +0800 Subject: [PATCH] feat(telegram): expose alert learning registry readback --- apps/api/src/api/v1/platform/operator_runs.py | 34 ++++ .../awoooi_priority_work_order_readback.py | 7 +- .../src/services/platform_operator_service.py | 162 +++++++++++++++++- .../telegram_alert_ai_automation_matrix.py | 72 +++++++- .../test_awooop_operator_timeline_labels.py | 96 +++++++++++ ...telegram_alert_ai_automation_matrix_api.py | 19 +- .../web/src/app/[locale]/awooop/runs/page.tsx | 129 +++++++++++++- docs/LOGBOOK.md | 16 ++ ...r-inserted-requirements-priority-ledger.md | 2 +- 9 files changed, 524 insertions(+), 13 deletions(-) diff --git a/apps/api/src/api/v1/platform/operator_runs.py b/apps/api/src/api/v1/platform/operator_runs.py index c681b80ba..2c4efee5c 100644 --- a/apps/api/src/api/v1/platform/operator_runs.py +++ b/apps/api/src/api/v1/platform/operator_runs.py @@ -141,6 +141,7 @@ class AiAlertCardDeliveryItem(BaseModel): learning_writeback_ready: bool = False learning_writeback_status: str = "not_evaluated" learning_writeback_refs: dict[str, Any] = Field(default_factory=dict) + learning_registry_bindings: list[dict[str, Any]] = Field(default_factory=list) source_refs: dict[str, Any] run_state: str | None = None agent_id: str | None = None @@ -170,17 +171,50 @@ class AiAlertCardDeliverySummary(BaseModel): mcp_evidence_ref_ready_total: int = 0 post_verifier_ref_ready_total: int = 0 learning_writeback_next_action: str = "none" + learning_registry_target_count: int = 0 + learning_registry_ready_target_count: int = 0 + learning_registry_missing_target_count: int = 0 + learning_registry_next_action: str = "none" latest_sent_at: datetime | None = None latest_queued_at: datetime | None = None production_write_count: int = 0 +class AiAlertCardLearningRegistryTarget(BaseModel): + target: str + status: str + consumer_surface: str + source_field: str + ready_receipt_count: int + latest_ref: str | None = None + target_write_performed: bool = False + metadata_only: bool = True + next_action: str + + +class AiAlertCardLearningRegistry(BaseModel): + schema_version: str + status: str + work_item_id: str + project_id: str + source_endpoint: str + consumer_context_route: str + target_count: int + ready_target_count: int + missing_target_count: int + ready_receipt_ref_count: int + next_action: str + targets: list[AiAlertCardLearningRegistryTarget] + operation_boundaries: dict[str, bool] + + class ListAiAlertCardsResponse(BaseModel): items: list[AiAlertCardDeliveryItem] total: int page: int per_page: int summary: AiAlertCardDeliverySummary + learning_registry: AiAlertCardLearningRegistry class OutboundReplyMarkupGapPrefix(BaseModel): diff --git a/apps/api/src/services/awoooi_priority_work_order_readback.py b/apps/api/src/services/awoooi_priority_work_order_readback.py index ad9528353..7eae0c282 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -339,10 +339,11 @@ _COMMANDER_INSERTED_REQUIREMENT_WORK_ITEMS: list[dict[str, Any]] = [ "TelegramGateway / AwoooP outbound receipt path;scripts/ops 4 條 legacy direct Bot API fallback " "已改為 AWOOI API / TelegramGateway receipt path;目前明確讀回 workflow direct send 0、" "ops script direct gap 0、API direct gap 0;/runs/ai-alert-cards 已補 KM / PlayBook / RAG / MCP " - "learning_writeback_refs read model。" + "learning_writeback_refs read model,並投影 KM / PlayBook / RAG / MCP / verifier / AI Agent " + "learning_registry consumer bindings。" ), - "acceptance": "Telegram alert matrix 顯示 total、DB receipt、AI route、controlled queue、manual/default gap、direct send gap 與 learning refs。", - "next_action": "把 Telegram alert learning refs 接到 KM / PlayBook / RAG / MCP registry readback,讓 AI Loop Agent 可重用這些 receipt。", + "acceptance": "Telegram alert matrix 顯示 total、DB receipt、AI route、controlled queue、manual/default gap、direct send gap、learning refs 與 registry readback。", + "next_action": "把 Telegram alert learning registry 接到 AI Loop Agent consumer context receipt,讓後續判斷可直接重用這些 receipt。", "mapped_workplan_id": "P0-006", }, { diff --git a/apps/api/src/services/platform_operator_service.py b/apps/api/src/services/platform_operator_service.py index bc75c3b87..6d89ff2e3 100644 --- a/apps/api/src/services/platform_operator_service.py +++ b/apps/api/src/services/platform_operator_service.py @@ -99,6 +99,38 @@ _CALLBACK_REPLY_CACHE_TTL_SECONDS = int( _AI_ALERT_CARD_CACHE_TTL_SECONDS = int( os.getenv("AWOOOP_AI_ALERT_CARD_CACHE_TTL_SECONDS", "20") ) +_AI_ALERT_CARD_LEARNING_REGISTRY_TARGETS: tuple[dict[str, str], ...] = ( + { + "target": "km", + "consumer_surface": "knowledge_memory_context", + "source_field": "km_entry_ref", + }, + { + "target": "playbook", + "consumer_surface": "playbook_trust_candidate_context", + "source_field": "playbook_candidate_ref", + }, + { + "target": "rag", + "consumer_surface": "rag_index_candidate_context", + "source_field": "rag_chunk_ref", + }, + { + "target": "mcp", + "consumer_surface": "mcp_audit_feedback_context", + "source_field": "mcp_evidence_ref", + }, + { + "target": "verifier", + "consumer_surface": "post_apply_verifier_feedback_context", + "source_field": "post_verifier_ref", + }, + { + "target": "ai_agent", + "consumer_surface": "autonomous_runtime_decision_context", + "source_field": "ai_agent_context_ref", + }, +) _INCIDENT_ID_RE = re.compile(r"\bINC-\d{8}-[A-Z0-9]{4,}\b") _REMEDIATION_STATUS_FILTERS = { "mcp_observed", @@ -1323,7 +1355,7 @@ async def list_ai_alert_card_delivery_readback( cache_key, ttl_seconds=_AI_ALERT_CARD_CACHE_TTL_SECONDS, ) - if cached_response is not None: + if cached_response is not None and "learning_registry" in cached_response: logger.info( "operator_ai_alert_card_delivery_readback_cache_hit", project_id=normalized_project_id, @@ -1439,12 +1471,26 @@ async def list_ai_alert_card_delivery_readback( event_type=normalized_event_type or None, lane=normalized_lane or None, ) + items = [_ai_alert_card_delivery_item(row) for row in rows] + learning_registry = _ai_alert_card_learning_registry_readback( + items=items, + project_id=normalized_project_id, + ) + summary["learning_registry_target_count"] = learning_registry["target_count"] + summary["learning_registry_ready_target_count"] = learning_registry[ + "ready_target_count" + ] + summary["learning_registry_missing_target_count"] = learning_registry[ + "missing_target_count" + ] + summary["learning_registry_next_action"] = learning_registry["next_action"] response = { - "items": [_ai_alert_card_delivery_item(row) for row in rows], + "items": items, "total": summary["total"], "page": normalized_page, "per_page": normalized_per_page, "summary": summary, + "learning_registry": learning_registry, } logger.info( "operator_ai_alert_card_delivery_readback_fetched", @@ -1519,6 +1565,10 @@ def _ai_alert_card_delivery_summary_from_row( if total > 0 and learning_writeback_missing_total == 0 else "ensure_ai_alert_cards_include_event_type_lane_and_delivery_receipt" ), + "learning_registry_target_count": 0, + "learning_registry_ready_target_count": 0, + "learning_registry_missing_target_count": 0, + "learning_registry_next_action": "none", "latest_sent_at": row.get("latest_sent_at"), "latest_queued_at": row.get("latest_queued_at"), "production_write_count": 0, @@ -1572,6 +1622,9 @@ def _ai_alert_card_delivery_item(row: Mapping[str, Any]) -> dict[str, Any]: ), "learning_writeback_status": learning_writeback_refs["status"], "learning_writeback_refs": learning_writeback_refs, + "learning_registry_bindings": _ai_alert_card_learning_registry_bindings( + learning_writeback_refs + ), "source_refs": source_refs, "run_state": row.get("run_state"), "agent_id": row.get("agent_id"), @@ -1642,6 +1695,9 @@ def _ai_alert_card_learning_writeback_refs( "post_verifier_ref": ( f"verifier://{project_id}/telegram-alert-delivery-receipt/{run_ref}" ), + "ai_agent_context_ref": ( + f"ai-agent://{project_id}/telegram-alert-learning-context/{run_ref}/{message_ref}" + ), "source_fingerprints": fingerprints if isinstance(fingerprints, list) else [], "alert_ids": alert_ids if isinstance(alert_ids, list) else [], "missing_required_fields": missing, @@ -1655,6 +1711,108 @@ def _ai_alert_card_learning_writeback_refs( } +def _ai_alert_card_learning_registry_bindings( + learning_refs: Mapping[str, Any], +) -> list[dict[str, Any]]: + """Project one alert-card receipt into KM / RAG / MCP consumer bindings.""" + bindings: list[dict[str, Any]] = [] + for target in _AI_ALERT_CARD_LEARNING_REGISTRY_TARGETS: + ref = str(learning_refs.get(target["source_field"]) or "") + ready = learning_refs.get("status") == "ready" and bool(ref) + bindings.append({ + "target": target["target"], + "status": "ready_for_consumer_context" if ready else "missing_ref", + "consumer_surface": target["consumer_surface"], + "source_field": target["source_field"], + "ref": ref or None, + "receipt_id": learning_refs.get("receipt_id") or "", + "work_item_id": learning_refs.get("work_item_id") or "CIR-P0-TG-001", + "metadata_only": True, + "target_write_performed": False, + "raw_payload_included": False, + "next_action": ( + "consume_metadata_receipt_in_ai_loop_context" + if ready + else "repair_missing_learning_ref_before_consumer_binding" + ), + }) + return bindings + + +def _ai_alert_card_learning_registry_readback( + *, + items: list[dict[str, Any]], + project_id: str, +) -> dict[str, Any]: + target_rollups: list[dict[str, Any]] = [] + ready_receipt_ref_count = 0 + for target in _AI_ALERT_CARD_LEARNING_REGISTRY_TARGETS: + refs = [ + binding.get("ref") + for item in items + for binding in item.get("learning_registry_bindings", []) + if binding.get("target") == target["target"] and binding.get("ref") + ] + ready_count = len(refs) + ready_receipt_ref_count += ready_count + target_rollups.append({ + "target": target["target"], + "status": "ready_for_consumer_context" if ready_count else "waiting_receipt", + "consumer_surface": target["consumer_surface"], + "source_field": target["source_field"], + "ready_receipt_count": ready_count, + "latest_ref": refs[0] if refs else None, + "target_write_performed": False, + "metadata_only": True, + "next_action": ( + "consume_metadata_receipt_in_ai_loop_context" + if ready_count + else "wait_for_ai_alert_card_learning_ref" + ), + }) + + ready_target_count = sum( + 1 for target in target_rollups if target["status"] == "ready_for_consumer_context" + ) + missing_target_count = len(_AI_ALERT_CARD_LEARNING_REGISTRY_TARGETS) - ready_target_count + return { + "schema_version": "ai_alert_card_learning_registry_readback_v1", + "status": ( + "learning_registry_ready" + if ready_target_count == len(_AI_ALERT_CARD_LEARNING_REGISTRY_TARGETS) + and bool(items) + else "waiting_ai_alert_card_receipts" + ), + "work_item_id": "CIR-P0-TG-001", + "project_id": project_id, + "source_endpoint": "/api/v1/platform/runs/ai-alert-cards", + "consumer_context_route": "ai_loop_agent.telegram_alert_learning_context", + "target_count": len(_AI_ALERT_CARD_LEARNING_REGISTRY_TARGETS), + "ready_target_count": ready_target_count, + "missing_target_count": missing_target_count, + "ready_receipt_ref_count": ready_receipt_ref_count, + "next_action": ( + "consume_telegram_alert_learning_registry_in_ai_loop_agent_context" + if ready_target_count == len(_AI_ALERT_CARD_LEARNING_REGISTRY_TARGETS) + and bool(items) + else "wait_for_ai_alert_card_learning_refs_before_registry_consume" + ), + "targets": target_rollups, + "operation_boundaries": { + "metadata_read_performed": True, + "km_write_performed": False, + "rag_index_write_performed": False, + "playbook_trust_write_performed": False, + "mcp_tool_call_performed": False, + "verifier_execution_performed": False, + "telegram_send_performed": False, + "raw_payload_included": False, + "secret_value_read": False, + "github_api_used": False, + }, + } + + async def _fetch_callback_reply_audit_summary( db: Any, *, diff --git a/apps/api/src/services/telegram_alert_ai_automation_matrix.py b/apps/api/src/services/telegram_alert_ai_automation_matrix.py index 383221a15..a0ca88639 100644 --- a/apps/api/src/services/telegram_alert_ai_automation_matrix.py +++ b/apps/api/src/services/telegram_alert_ai_automation_matrix.py @@ -84,6 +84,9 @@ def load_latest_telegram_alert_ai_automation_matrix( learning_writeback_refs_present = bool( source_proof["ai_alert_card_learning_writeback_refs_present"] ) + learning_registry_present = bool( + source_proof["ai_alert_card_learning_registry_readback_present"] + ) direct_gap_count = int(egress_summary["direct_bot_api_call_count"]) direct_gap_file_count = int(egress_summary["direct_bot_api_file_count"]) @@ -242,6 +245,34 @@ def load_latest_telegram_alert_ai_automation_matrix( "apps/api/src/db/awooop_models.py", ], }, + { + "surface_id": "telegram_alert_learning_registry_readback", + "priority_work_item_id": "CIR-P0-TG-001", + "surface_kind": "learning_registry_readback", + "status": ( + "registry_readback_present" + if learning_registry_present + else "registry_readback_missing" + ), + "known_item_count": source_proof[ + "ai_alert_card_learning_registry_readback_count" + ], + "db_or_log_receipt": "ready_awooop_outbound_message_learning_refs", + "ai_route": "ready_ai_loop_agent_consumer_context", + "controlled_queue": "ready_metadata_only_consumer_binding", + "post_verifier": "ready_registry_source_contract_present", + "learning_writeback": ( + "ready_km_rag_playbook_mcp_verifier_ai_agent_registry" + if learning_registry_present + else "missing_registry_readback" + ), + "manual_default_gap_count": 0, + "direct_send_gap_count": 0, + "evidence_refs": [ + "apps/api/src/services/platform_operator_service.py", + "/api/v1/platform/runs/ai-alert-cards", + ], + }, ] next_priority_action = _next_priority_action( @@ -249,6 +280,7 @@ def load_latest_telegram_alert_ai_automation_matrix( ops_script_gap_count=ops_script_gap_count, direct_gap_count=direct_gap_count, learning_writeback_refs_present=learning_writeback_refs_present, + learning_registry_present=learning_registry_present, ) next_controlled_actions = _ordered_controlled_actions(next_priority_action) summary = _build_summary( @@ -306,6 +338,12 @@ def load_latest_telegram_alert_ai_automation_matrix( ( ( ( + "Manual/default semantics are only acceptable for critical/break-glass or historical evidence; " + "KM / PlayBook / RAG / MCP / verifier / AI Agent registry readback is present, " + "so the next P0 step is AI Loop Agent consumer context." + ) + if learning_registry_present + else ( "Manual/default semantics are only acceptable for critical/break-glass or historical evidence; " "learning refs are present, so the next P0 step is KM / PlayBook / RAG / MCP registry readback." ) @@ -443,6 +481,14 @@ def _inspect_source_contract(repo_root: Path) -> dict[str, int | bool]: "apps/api/src/api/v1/platform/operator_runs.py", "learning_writeback_ready_total", ), + "ai_alert_card_learning_registry_readback_present": ( + "apps/api/src/services/platform_operator_service.py", + "ai_alert_card_learning_registry_readback_v1", + ), + "ai_alert_card_ai_agent_context_ref_present": ( + "apps/api/src/services/platform_operator_service.py", + "ai_agent_context_ref", + ), } result: dict[str, int | bool] = {} for key, (relative_path, marker) in source_checks.items(): @@ -453,6 +499,10 @@ def _inspect_source_contract(repo_root: Path) -> dict[str, int | bool]: result["ai_alert_card_delivery_readback_endpoint_count"] = 1 if present else 0 if key == "ai_alert_card_learning_writeback_refs_present": result["ai_alert_card_learning_writeback_refs_count"] = 1 if present else 0 + if key == "ai_alert_card_learning_registry_readback_present": + result["ai_alert_card_learning_registry_readback_count"] = ( + 1 if present else 0 + ) required = [key for key, value in result.items() if key.endswith("_present") and value is not True] if required: @@ -478,6 +528,7 @@ def _next_priority_action( ops_script_gap_count: int, direct_gap_count: int, learning_writeback_refs_present: bool, + learning_registry_present: bool, ) -> dict[str, Any]: if api_direct_gap_count > 0: return { @@ -519,14 +570,24 @@ def _next_priority_action( "requires_runtime_send": False, "post_verifier": "/api/v1/platform/runs/ai-alert-cards readback shows delivery_receipt_readback_required and learning writeback refs.", } + if not learning_registry_present: + return { + "action_id": "promote_telegram_alert_learning_refs_to_km_rag_mcp_registry", + "scope": "AI alert card receipt refs -> KM / PlayBook / RAG / MCP registry readback", + "priority": "P0", + "why": "Alert cards now expose learning refs; the next closure is registry/readback integration for AI Loop Agent reuse.", + "requires_secret": False, + "requires_runtime_send": False, + "post_verifier": "KM / PlayBook / RAG / MCP registry readback includes CIR-P0-TG-001 alert-card receipt refs.", + } return { - "action_id": "promote_telegram_alert_learning_refs_to_km_rag_mcp_registry", - "scope": "AI alert card receipt refs -> KM / PlayBook / RAG / MCP registry readback", + "action_id": "consume_telegram_alert_learning_registry_in_ai_loop_agent_context", + "scope": "KM / PlayBook / RAG / MCP / verifier registry -> AI Loop Agent consumer context", "priority": "P0", - "why": "Alert cards now expose learning refs; the next closure is registry/readback integration for AI Loop Agent reuse.", + "why": "Telegram alert learning registry is readable; the next closure is AI Loop Agent consumer context reuse and apply receipt.", "requires_secret": False, "requires_runtime_send": False, - "post_verifier": "KM / PlayBook / RAG / MCP registry readback includes CIR-P0-TG-001 alert-card receipt refs.", + "post_verifier": "AI Loop Agent context readback references the CIR-P0-TG-001 Telegram alert learning registry.", } @@ -589,6 +650,9 @@ def _build_summary( "ai_alert_card_learning_writeback_refs_count": source_proof[ "ai_alert_card_learning_writeback_refs_count" ], + "ai_alert_card_learning_registry_readback_count": source_proof[ + "ai_alert_card_learning_registry_readback_count" + ], "next_priority_action_id": next_priority_action["action_id"], "next_priority_work_item_id": "CIR-P0-TG-001", } diff --git a/apps/api/tests/test_awooop_operator_timeline_labels.py b/apps/api/tests/test_awooop_operator_timeline_labels.py index d7fc86e1c..3abec9da5 100644 --- a/apps/api/tests/test_awooop_operator_timeline_labels.py +++ b/apps/api/tests/test_awooop_operator_timeline_labels.py @@ -991,6 +991,22 @@ def test_ai_alert_card_delivery_item_uses_metadata_without_raw_content() -> None assert item["learning_writeback_refs"]["post_verifier_ref"].endswith( "5c0306e0-591a-5445-9a33-80f499426b38" ) + assert item["learning_writeback_refs"]["ai_agent_context_ref"].startswith( + "ai-agent://awoooi/telegram-alert-learning-context/" + ) + registry_targets = { + binding["target"]: binding for binding in item["learning_registry_bindings"] + } + assert set(registry_targets) == { + "km", + "playbook", + "rag", + "mcp", + "verifier", + "ai_agent", + } + assert registry_targets["ai_agent"]["status"] == "ready_for_consumer_context" + assert registry_targets["ai_agent"]["target_write_performed"] is False assert item["source_refs"]["alert_ids"] == [ "wazuh_dashboard_api_readback_degraded" ] @@ -1033,6 +1049,10 @@ def test_ai_alert_card_delivery_summary_keeps_no_false_green_status() -> None: assert summary["learning_writeback_next_action"] == ( "ensure_ai_alert_cards_include_event_type_lane_and_delivery_receipt" ) + assert summary["learning_registry_target_count"] == 0 + assert summary["learning_registry_ready_target_count"] == 0 + assert summary["learning_registry_missing_target_count"] == 0 + assert summary["learning_registry_next_action"] == "none" assert summary["production_write_count"] == 0 @@ -1093,6 +1113,10 @@ def test_list_ai_alert_cards_response_preserves_delivery_metadata() -> None: "verifier://awoooi/telegram-alert-delivery-receipt/" f"{run_id}" ), + "ai_agent_context_ref": ( + "ai-agent://awoooi/telegram-alert-learning-context/" + f"{run_id}/{message_id}" + ), "source_fingerprints": [], "alert_ids": ["wazuh_dashboard_api_readback_degraded"], "missing_required_fields": [], @@ -1100,6 +1124,24 @@ def test_list_ai_alert_cards_response_preserves_delivery_metadata() -> None: "runtime_send_performed": False, "next_action": "none", }, + "learning_registry_bindings": [ + { + "target": "km", + "status": "ready_for_consumer_context", + "consumer_surface": "knowledge_memory_context", + "source_field": "km_entry_ref", + "ref": ( + "km://awoooi/telegram-ai-alert-card/" + "wazuh_dashboard_api_readback_degraded" + ), + "receipt_id": str(message_id), + "work_item_id": "CIR-P0-TG-001", + "metadata_only": True, + "target_write_performed": False, + "raw_payload_included": False, + "next_action": "consume_metadata_receipt_in_ai_loop_context", + } + ], "source_refs": { "alert_ids": ["wazuh_dashboard_api_readback_degraded"], }, @@ -1137,10 +1179,61 @@ def test_list_ai_alert_cards_response_preserves_delivery_metadata() -> None: "mcp_evidence_ref_ready_total": 1, "post_verifier_ref_ready_total": 1, "learning_writeback_next_action": "none", + "learning_registry_target_count": 6, + "learning_registry_ready_target_count": 6, + "learning_registry_missing_target_count": 0, + "learning_registry_next_action": ( + "consume_telegram_alert_learning_registry_in_ai_loop_agent_context" + ), "latest_sent_at": datetime(2026, 6, 25, 9, 40, 5), "latest_queued_at": datetime(2026, 6, 25, 9, 40, 0), "production_write_count": 0, }, + "learning_registry": { + "schema_version": "ai_alert_card_learning_registry_readback_v1", + "status": "learning_registry_ready", + "work_item_id": "CIR-P0-TG-001", + "project_id": "awoooi", + "source_endpoint": "/api/v1/platform/runs/ai-alert-cards", + "consumer_context_route": ( + "ai_loop_agent.telegram_alert_learning_context" + ), + "target_count": 6, + "ready_target_count": 6, + "missing_target_count": 0, + "ready_receipt_ref_count": 6, + "next_action": ( + "consume_telegram_alert_learning_registry_in_ai_loop_agent_context" + ), + "targets": [ + { + "target": "km", + "status": "ready_for_consumer_context", + "consumer_surface": "knowledge_memory_context", + "source_field": "km_entry_ref", + "ready_receipt_count": 1, + "latest_ref": ( + "km://awoooi/telegram-ai-alert-card/" + "wazuh_dashboard_api_readback_degraded" + ), + "target_write_performed": False, + "metadata_only": True, + "next_action": "consume_metadata_receipt_in_ai_loop_context", + } + ], + "operation_boundaries": { + "metadata_read_performed": True, + "km_write_performed": False, + "rag_index_write_performed": False, + "playbook_trust_write_performed": False, + "mcp_tool_call_performed": False, + "verifier_execution_performed": False, + "telegram_send_performed": False, + "raw_payload_included": False, + "secret_value_read": False, + "github_api_used": False, + }, + }, }) dumped = response.model_dump(mode="json") @@ -1152,8 +1245,11 @@ def test_list_ai_alert_cards_response_preserves_delivery_metadata() -> None: assert dumped["items"][0]["learning_writeback_refs"]["work_item_id"] == ( "CIR-P0-TG-001" ) + assert dumped["items"][0]["learning_registry_bindings"][0]["target"] == "km" assert dumped["summary"]["delivery_receipt_required_total"] == 1 assert dumped["summary"]["learning_writeback_ready_total"] == 1 + assert dumped["summary"]["learning_registry_ready_target_count"] == 6 + assert dumped["learning_registry"]["ready_target_count"] == 6 assert dumped["summary"]["production_write_count"] == 0 diff --git a/apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py b/apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py index 8d1a4d9f3..750e8286d 100644 --- a/apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py +++ b/apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py @@ -32,7 +32,7 @@ def test_telegram_alert_ai_automation_matrix_loader_returns_gap_matrix(): } summary = payload["summary"] - assert summary["telegram_alert_surface_count"] == 5 + assert summary["telegram_alert_surface_count"] == 6 assert summary["known_direct_send_gap_count"] == 0 assert summary["known_direct_send_gap_file_count"] == 0 assert summary["workflow_direct_send_gap_count"] == 0 @@ -53,8 +53,9 @@ def test_telegram_alert_ai_automation_matrix_loader_returns_gap_matrix(): assert summary["runtime_write_performed_count"] == 0 assert summary["ai_alert_card_delivery_readback_endpoint_count"] == 1 assert summary["ai_alert_card_learning_writeback_refs_count"] == 1 + assert summary["ai_alert_card_learning_registry_readback_count"] == 1 assert summary["next_priority_action_id"] == ( - "promote_telegram_alert_learning_refs_to_km_rag_mcp_registry" + "consume_telegram_alert_learning_registry_in_ai_loop_agent_context" ) matrix = {item["surface_id"]: item for item in payload["matrix"]} @@ -81,6 +82,12 @@ def test_telegram_alert_ai_automation_matrix_loader_returns_gap_matrix(): assert matrix["telegram_ai_alert_card_delivery_readback_endpoint"][ "learning_writeback" ] == "ready_delivery_receipt_summary_and_learning_writeback_refs" + assert matrix["telegram_alert_learning_registry_readback"]["status"] == ( + "registry_readback_present" + ) + assert matrix["telegram_alert_learning_registry_readback"]["learning_writeback"] == ( + "ready_km_rag_playbook_mcp_verifier_ai_agent_registry" + ) assert payload["direct_send_gap_refs"] == [] assert not any( @@ -119,6 +126,11 @@ def test_telegram_matrix_source_contract_supports_container_layout(tmp_path): "learning_writeback_ready_total\n", encoding="utf-8", ) + (tmp_path / "src/services/platform_operator_service.py").write_text( + "ai_alert_card_learning_registry_readback_v1\n" + "ai_agent_context_ref\n", + encoding="utf-8", + ) proof = _inspect_source_contract(tmp_path) @@ -130,6 +142,9 @@ def test_telegram_matrix_source_contract_supports_container_layout(tmp_path): assert proof["ai_alert_card_delivery_readback_endpoint_count"] == 1 assert proof["ai_alert_card_learning_writeback_refs_present"] is True assert proof["ai_alert_card_learning_writeback_refs_count"] == 1 + assert proof["ai_alert_card_learning_registry_readback_present"] is True + assert proof["ai_alert_card_learning_registry_readback_count"] == 1 + assert proof["ai_alert_card_ai_agent_context_ref_present"] is True def test_telegram_alert_ai_automation_matrix_endpoint_returns_readback(): diff --git a/apps/web/src/app/[locale]/awooop/runs/page.tsx b/apps/web/src/app/[locale]/awooop/runs/page.tsx index a883eaed5..401be1a86 100644 --- a/apps/web/src/app/[locale]/awooop/runs/page.tsx +++ b/apps/web/src/app/[locale]/awooop/runs/page.tsx @@ -554,11 +554,35 @@ interface AiAlertCardDeliverySummary { delivery_receipt_required_total: number; runtime_write_gate_open_count: number; runtime_write_allowed: boolean; + learning_writeback_ready_total?: number; + learning_writeback_missing_total?: number; + km_writeback_ref_ready_total?: number; + playbook_writeback_ref_ready_total?: number; + rag_chunk_ref_ready_total?: number; + mcp_evidence_ref_ready_total?: number; + post_verifier_ref_ready_total?: number; + learning_registry_target_count?: number; + learning_registry_ready_target_count?: number; + learning_registry_missing_target_count?: number; + learning_registry_next_action?: string; latest_sent_at?: string | null; latest_queued_at?: string | null; production_write_count: number; } +interface AiAlertCardLearningRegistryBinding { + target: string; + status: string; + consumer_surface: string; + source_field: string; + ref?: string | null; + latest_ref?: string | null; + ready_receipt_count?: number; + target_write_performed?: boolean; + metadata_only?: boolean; + next_action?: string; +} + interface AiAlertCardDeliveryItem { message_id: string; run_id: string; @@ -578,6 +602,10 @@ interface AiAlertCardDeliveryItem { runtime_write_allowed: boolean; candidate_only: boolean; delivery_receipt_readback_required: boolean; + learning_writeback_ready?: boolean; + learning_writeback_status?: string; + learning_writeback_refs?: Record; + learning_registry_bindings?: AiAlertCardLearningRegistryBinding[]; source_refs: Record; run_state?: string | null; agent_id?: string | null; @@ -585,12 +613,29 @@ interface AiAlertCardDeliveryItem { run_detail_href?: string | null; } +interface AiAlertCardLearningRegistry { + schema_version: string; + status: string; + work_item_id: string; + project_id: string; + source_endpoint: string; + consumer_context_route: string; + target_count: number; + ready_target_count: number; + missing_target_count: number; + ready_receipt_ref_count: number; + next_action: string; + targets: AiAlertCardLearningRegistryBinding[]; + operation_boundaries?: Record; +} + interface AiAlertCardsResponse { items?: AiAlertCardDeliveryItem[]; total?: number; page?: number; per_page?: number; summary?: AiAlertCardDeliverySummary | null; + learning_registry?: AiAlertCardLearningRegistry | null; } interface AiRoutePolicyItem { @@ -3620,11 +3665,13 @@ function CallbackReplyEvidencePanel({ function AiAlertCardDeliveryPanel({ items, summary, + learningRegistry, loading, error, }: { items: AiAlertCardDeliveryItem[]; summary?: AiAlertCardDeliverySummary | null; + learningRegistry?: AiAlertCardLearningRegistry | null; loading: boolean; error: string | null; }) { @@ -3640,6 +3687,17 @@ function AiAlertCardDeliveryPanel({ const failed = summary?.failed_total ?? 0; const pending = summary?.pending_total ?? 0; const runtimeGateOpen = summary?.runtime_write_gate_open_count ?? 0; + const registryReady = learningRegistry?.ready_target_count + ?? summary?.learning_registry_ready_target_count + ?? 0; + const registryTotal = learningRegistry?.target_count + ?? summary?.learning_registry_target_count + ?? 0; + const registryStatus = registryTotal > 0 && registryReady === registryTotal + ? "Registry ready" + : registryTotal > 0 + ? "Registry waiting" + : "Registry empty"; const statusLabel = total === 0 ? "尚無送達讀回" : failed > 0 @@ -3664,6 +3722,14 @@ function AiAlertCardDeliveryPanel({ const targetLabel = (target: string) => ({ wazuh_dashboard_api: "Wazuh 儀表板/API 讀回", }[target] ?? "未分類目標"); + const registryTargetLabel = (target: string) => ({ + km: "KM", + playbook: "PlayBook", + rag: "RAG", + mcp: "MCP", + verifier: "Verifier", + ai_agent: "AI Agent", + }[target] ?? target); return (
執行閘門 {runtimeGateOpen} + + {registryReady}/{registryTotal} registry + @@ -3696,7 +3765,11 @@ function AiAlertCardDeliveryPanel({ { label: "總數", value: total, detail: `最新 ${latest}` }, { label: "已送達", value: sent, detail: "Telegram 鏡像已送達" }, { label: "失敗", value: failed, detail: "需查送達錯誤" }, - { label: "等待", value: pending, detail: `影子 ${summary?.shadow_total ?? 0}` }, + { + label: "Registry", + value: registryReady, + detail: `${registryStatus} · refs ${learningRegistry?.ready_receipt_ref_count ?? 0}`, + }, ].map((metric) => (

{metric.label}

@@ -3706,6 +3779,46 @@ function AiAlertCardDeliveryPanel({ ))}
+ {learningRegistry && ( +
+
+
+

AI Loop learning registry

+

+ {learningRegistry.consumer_context_route} · {learningRegistry.next_action} +

+
+ + {learningRegistry.ready_target_count}/{learningRegistry.target_count} + +
+
+ {learningRegistry.targets.map((target) => { + const ready = target.status === "ready_for_consumer_context"; + return ( +
+

+ {registryTargetLabel(target.target)} +

+

+ {ready ? "ready" : "waiting"} · {target.ready_receipt_count ?? 0} +

+

+ {target.latest_ref ?? target.ref ?? target.consumer_surface} +

+
+ ); + })} +
+
+ )} + {error ? (
讀回失敗:{error}
) : items.length > 0 ? ( @@ -3750,6 +3863,14 @@ function AiAlertCardDeliveryPanel({

目標:{targetLabel(item.target)}

證據索引:{sourceRefCount}

執行閘門:{item.runtime_write_gate_count}

+

+ Registry: + + {item.learning_registry_bindings?.filter((binding) => ( + binding.status === "ready_for_consumer_context" + )).length ?? 0} + +

(null); const [aiAlertCardItems, setAiAlertCardItems] = useState([]); const [aiAlertCardSummary, setAiAlertCardSummary] = useState(null); + const [aiAlertLearningRegistry, setAiAlertLearningRegistry] = + useState(null); const [aiAlertCardLoading, setAiAlertCardLoading] = useState(true); const [aiAlertCardError, setAiAlertCardError] = useState(null); const [aiRouteStatus, setAiRouteStatus] = useState(null); @@ -4437,15 +4560,18 @@ export default function RunsPage() { const aiAlertData: AiAlertCardsResponse = await aiAlertRes.json(); setAiAlertCardItems(Array.isArray(aiAlertData.items) ? aiAlertData.items : []); setAiAlertCardSummary(aiAlertData.summary ?? null); + setAiAlertLearningRegistry(aiAlertData.learning_registry ?? null); setAiAlertCardError(null); } else { setAiAlertCardItems([]); setAiAlertCardSummary(null); + setAiAlertLearningRegistry(null); setAiAlertCardError(`端點尚未部署或暫時不可用(HTTP ${aiAlertRes.status})`); } } catch (aiAlertError) { setAiAlertCardItems([]); setAiAlertCardSummary(null); + setAiAlertLearningRegistry(null); setAiAlertCardError("無法讀回 AI 事件卡送達狀態"); } finally { setAiAlertCardLoading(false); @@ -4794,6 +4920,7 @@ export default function RunsPage() { diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index f7cce8237..81a6e40fd 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,19 @@ +## 2026-07-02 — 20:18 CIR-P0-TG-001 Telegram alert learning registry readback + +**完成內容**: +- `/api/v1/platform/runs/ai-alert-cards` 已把每筆 AI alert card receipt 的 `learning_writeback_refs` 再投影成 `learning_registry_bindings`,覆蓋 KM、PlayBook、RAG、MCP、verifier、AI Agent 六個 consumer target。 +- 同一端點已新增 top-level `learning_registry` readback,顯示 target count、ready target、ready receipt refs、consumer context route 與 operation boundaries;此 readback 只讀 metadata,不寫 KM / RAG index / PlayBook trust、不呼叫 MCP、不發 Telegram。 +- `telegram_alert_ai_automation_matrix` source contract 已要求 registry readback 與 `ai_agent_context_ref` marker;direct send gap 維持 0 後,下一個 P0 action 推進為 `consume_telegram_alert_learning_registry_in_ai_loop_agent_context`。 +- `awoooi-priority-work-order-readback` 與 commander inserted requirements ledger 已同步:`CIR-P0-TG-001` 下一步不再停在 registry readback,而是 AI Loop Agent consumer context receipt。 + +**驗證**: +- `DATABASE_URL=postgresql+asyncpg://test:test@localhost/test PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py apps/api/tests/test_awooop_operator_timeline_labels.py apps/api/tests/test_awoooi_priority_work_order_readback_api.py ops/runner/test_cd_controlled_runtime_profile.py -q -p no:cacheprovider`:`131 passed`。 +- `pnpm --dir apps/web typecheck`:通過。 +- `python3.11 -m py_compile apps/api/src/api/v1/platform/operator_runs.py apps/api/src/services/platform_operator_service.py apps/api/src/services/telegram_alert_ai_automation_matrix.py apps/api/src/services/awoooi_priority_work_order_readback.py`、`python3.11 -c "import yaml; yaml.safe_load(open('.gitea/workflows/cd.yaml'))"`、`git diff --check`:通過。 + +**仍維持**: +- 未送 Telegram、未呼叫 Bot API、未讀 secret / token / `.env` / raw sessions / SQLite / auth;未寫 production DB、未改 receiver route、未 workflow_dispatch、未重啟主機 / VM / Docker / Nginx / K3s / DB / firewall。 + ## 2026-07-02 — 20:02 P0-006 Alertmanager 主告警鏈路恢復 public webhook **完成內容**: diff --git a/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md b/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md index 206c7fc38..9f0059c6d 100644 --- a/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md +++ b/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md @@ -88,7 +88,7 @@ | 2 | CIR-P0-AILOOP-002 | P0 | 「所有 AI 自動化完整流程、節點必須完整且詳盡紀錄」 | 每次 AI 自動化事件記錄 trace_id、run_id、work_item_id、節點狀態、timestamp、evidence refs、verifier、rollback、KM/PlayBook trust writeback | 已升為 API/UI 可見 P0 工作項;節點 schema 待補 | 建立 AI automation node receipt schema,先套用 Telegram alert 與 P0 runtime lanes | | 3 | CIR-P0-LOG-001 | P0 | 「所有專案、產品、網站、服務、套件、工具的 LOG 都要收進去」 | 建立全域 LOG source registry 與 ingestion adapters,含 redaction、retention、freshness、last_success、owner lane | 已升為 API/UI 可見 P0 工作項 | 先做 metadata-only LOG source registry,不讀 secret / raw payload | | 4 | CIR-P0-LOG-002 | P0 | 「LOG 要貼標、分類、分群,讓 AI Agent 可以處理」 | LOG 自動貼 product/project/site/service/package/tool、severity、lane、confidence、AI route、cluster_id、fingerprint | 已升為 API/UI 可見 P0 工作項 | 定義 tag taxonomy 與 clustering readback,先覆蓋 Telegram、CPU、backup、freshness | -| 5 | CIR-P0-TG-001 | P0 | 「所有發送到 Telegram 的監控告警是否全面清查、完整寫入 DB 或日誌、符合 AI 自動化?」 | 每個 Telegram 告警映射 DB/log receipt、AI route、controlled queue、work item、post verifier、learning writeback;direct send / manual default 列缺口 | workflow / ops / API direct send 已收斂到 0;`/runs/ai-alert-cards` 已補 KM / PlayBook / RAG / MCP `learning_writeback_refs` read model | 接 KM / PlayBook / RAG / MCP registry readback,人工預設只允許 critical / break-glass 或歷史證據 | +| 5 | CIR-P0-TG-001 | P0 | 「所有發送到 Telegram 的監控告警是否全面清查、完整寫入 DB 或日誌、符合 AI 自動化?」 | 每個 Telegram 告警映射 DB/log receipt、AI route、controlled queue、work item、post verifier、learning writeback;direct send / manual default 列缺口 | workflow / ops / API direct send 已收斂到 0;`/runs/ai-alert-cards` 已補 KM / PlayBook / RAG / MCP `learning_writeback_refs` read model,並投影 KM / PlayBook / RAG / MCP / verifier / AI Agent registry bindings | 接 AI Loop Agent consumer context receipt,人工預設只允許 critical / break-glass 或歷史證據 | | 6 | CIR-P0-IA-001 | P0 | 「IwoooS 被移除、導航列菜單被移除,且沒有整合到相關頁面」 | 建立 IA/nav removal recovery ledger:被移除 menu/route/page 必須有保留、合併、替代目的地、redirect、權限邊界與 smoke | 已升為 API/UI 可見 P0 工作項;route/menu 盤點待補 | 盤點 navigation config、route list 與 removed-page destination,補 readback | | 7 | CIR-P1-AUTO-001 | P1 | 「不能偏離不代表一定要每一個工作完成,才能進行下一個」 | 建立主線並行推進規則:不離開 P0/P1 主線,但依賴允許時可同時推 API/UI、LOG、Telegram、KM/RAG/MCP/PlayBook、verifier | 已升為 API/UI 可見 P1 工作項 | 後續回報以 active lane + evidence 更新,不等待單一項完全結束 | | 8 | CIR-P1-UI-001 | P1 | 「UI/UX 工作項目不見;整個前端頁面一大堆文字,要主流專業產品做法」 | UI/UX 成為主線:first-viewport cockpit、cards/flow rows、少文字牆、route/nav 一致、desktop/mobile smoke | 已升為 API/UI spotlight | 先讓 AI Loop / LOG / Telegram / IA 工作項在 Work Items spotlight 可見,再排 UI smoke |