From e2447d49a0108b4dfa40aee5bf411d08f523f904 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 2 Jul 2026 19:06:10 +0800 Subject: [PATCH] feat(awooop): close ai automation node receipts with live readback --- apps/api/src/api/v1/agents.py | 16 +- .../awoooi_priority_work_order_readback.py | 289 +++++++++++++++++- ...awoooi_priority_work_order_readback_api.py | 120 +++++++- 3 files changed, 407 insertions(+), 18 deletions(-) diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index d95b7b833..e80eab364 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -339,6 +339,7 @@ from src.services.awoooi_gitea_onboarding_warning_step_template_copy_receipt imp load_latest_awoooi_gitea_onboarding_warning_step_template_copy_receipt, ) from src.services.awoooi_priority_work_order_readback import ( + apply_ai_automation_live_closure_readbacks, apply_ai_loop_current_blocker_execution_queue, apply_controlled_cd_lane_live_metric_readback, apply_harbor_registry_controlled_recovery_preflight, @@ -1094,7 +1095,8 @@ async def get_delivery_closure_workbench() -> dict[str, Any]: "讀取已提交的 AWOOOI P0/P1 主線工作順序快照;此端點只回傳" "目前 active P0、已關閉 P0、下一步順序、禁止事項與 evidence refs。" "它會讀取 StockPlatform public HTTPS health/freshness/ingestion live " - "readback,並讀取 110 node_exporter 的 controlled CD lane textfile metrics;" + "readback、110 node_exporter controlled CD lane textfile metrics," + "以及 AI LOG controlled writeback executor / verifier / consumer receipts;" "不讀 raw sessions / SQLite、不呼叫 GitHub / Gitea live API、不 SSH、" "不讀 secret、不註冊 runner、不觸發 workflow、不操作 host / Docker / K8s / DB / firewall。" ), @@ -1135,6 +1137,18 @@ async def get_awoooi_priority_work_order_readback() -> dict[str, Any]: payload, controlled_cd_lane_metrics, ) + post_write_verifier = await asyncio.to_thread( + load_latest_ai_agent_log_post_write_verifier_dry_run + ) + consumer_readback = ( + await load_latest_ai_agent_log_controlled_writeback_consumer_readback() + ) + apply_ai_automation_live_closure_readbacks( + payload, + executor_readback=log_executor, + verifier_readback=post_write_verifier, + consumer_readback=consumer_readback, + ) return redact_public_lan_topology(payload) except FileNotFoundError as exc: raise HTTPException( 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 8fe86f7d2..338a98526 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -3528,6 +3528,144 @@ def apply_ai_automation_node_receipts(payload: dict[str, Any]) -> None: _apply_ai_automation_node_receipts(payload) +def apply_ai_automation_live_closure_readbacks( + payload: dict[str, Any], + *, + executor_readback: dict[str, Any] | None = None, + verifier_readback: dict[str, Any] | None = None, + consumer_readback: dict[str, Any] | None = None, +) -> None: + """Overlay live AI-loop executor/verifier/consumer readbacks onto receipts.""" + + summary = _dict(payload.setdefault("summary", {})) + rollups = _dict(payload.setdefault("rollups", {})) + state = _dict(payload.setdefault("mainline_execution_state", {})) + + executor = _dict(executor_readback) + executor_rollups = _dict(executor.get("rollups")) + executor_active_blockers = _strings(executor.get("active_blockers")) + if executor: + summary["ai_automation_executor_status"] = str(executor.get("status") or "") + summary["ai_automation_executor_ready_execution_batch_count"] = _int( + executor_rollups.get("ready_execution_batch_count") + ) + summary["ai_automation_executor_post_apply_verifier_ready_count"] = _int( + executor_rollups.get("post_apply_verifier_ready_count") + ) + summary["ai_automation_executor_log_source_grouping_key_count"] = _int( + executor_rollups.get("log_source_grouping_key_count") + ) + summary["ai_automation_executor_metadata_only_log_source_tagging_ready"] = bool( + executor_rollups.get("metadata_only_log_source_tagging_ready") is True + ) + summary["ai_automation_executor_controlled_dispatch_ready"] = bool( + executor_rollups.get("controlled_executor_dispatch_ready") is True + ) + summary["ai_automation_executor_runtime_dispatch_performed"] = bool( + executor_rollups.get("runtime_dispatch_performed") is True + ) + summary["ai_automation_executor_active_blockers"] = executor_active_blockers + rollups["ai_automation_executor_active_blocker_count"] = len( + executor_active_blockers + ) + state["ai_automation_executor_controlled_dispatch_ready"] = summary[ + "ai_automation_executor_controlled_dispatch_ready" + ] + + verifier = _dict(verifier_readback) + verifier_rollups = _dict(verifier.get("rollups")) + verifier_active_blockers = _strings(verifier.get("active_blockers")) + if verifier: + summary["ai_automation_post_write_verifier_status"] = str( + verifier.get("status") or "" + ) + summary["ai_automation_post_write_verifier_dry_run_ready"] = bool( + verifier_rollups.get("dry_run_ready") is True + ) + summary["ai_automation_post_write_verifier_passed_node_count"] = _int( + verifier_rollups.get("passed_verifier_node_count") + ) + summary["ai_automation_post_write_verifier_verified_target_count"] = _int( + verifier_rollups.get("verified_target_count") + ) + summary["ai_automation_post_write_verifier_verified_candidate_receipt_count"] = ( + _int(verifier_rollups.get("verified_candidate_receipt_count")) + ) + summary["ai_automation_post_write_verifier_active_blockers"] = ( + verifier_active_blockers + ) + rollups["ai_automation_post_write_verifier_active_blocker_count"] = len( + verifier_active_blockers + ) + state["ai_automation_post_write_verifier_dry_run_ready"] = summary[ + "ai_automation_post_write_verifier_dry_run_ready" + ] + + consumer = _dict(consumer_readback) + consumer_rollups = _dict(consumer.get("rollups")) + consumer_control = _dict(consumer.get("controlled_consume")) + consumer_active_blockers = _strings(consumer.get("active_blockers")) + if consumer: + context_receipt_counts = { + "km": _int(consumer_rollups.get("km_context_receipt_write_count")), + "rag": _int(consumer_rollups.get("rag_context_receipt_write_count")), + "playbook": _int( + consumer_rollups.get("playbook_context_receipt_write_count") + ), + "mcp": _int(consumer_rollups.get("mcp_context_receipt_write_count")), + "verifier": _int( + consumer_rollups.get("verifier_context_receipt_write_count") + ), + "ai_agent": _int( + consumer_rollups.get("ai_agent_context_receipt_write_count") + ), + } + summary["ai_automation_consumer_status"] = str(consumer.get("status") or "") + summary["ai_automation_consumer_readback_ready"] = bool( + consumer_rollups.get("controlled_consumer_readback_ready") is True + ) + summary["ai_automation_consumer_controlled_consume_allowed"] = bool( + consumer_control.get("controlled_consume_allowed") is True + ) + summary["ai_automation_consumer_runtime_target_write_performed"] = bool( + consumer_rollups.get("runtime_target_write_performed") is True + or consumer_control.get("runtime_target_write_performed") is True + ) + summary["ai_automation_consumer_ready_target_count"] = _int( + consumer_rollups.get("ready_target_count") + ) + summary["ai_automation_consumer_target_context_receipt_write_count"] = _int( + consumer_rollups.get("target_context_receipt_write_count") + ) + summary["ai_automation_consumer_post_apply_verifier_ref_count"] = _int( + consumer_rollups.get("post_apply_verifier_ref_count") + ) + summary["ai_automation_consumer_context_receipt_counts"] = ( + context_receipt_counts + ) + summary["ai_automation_consumer_apply_route"] = str( + consumer_control.get("consumer_apply_route") or "" + ) + summary["ai_automation_consumer_active_blockers"] = consumer_active_blockers + rollups["ai_automation_consumer_active_blocker_count"] = len( + consumer_active_blockers + ) + rollups["ai_automation_consumer_ready_target_count"] = summary[ + "ai_automation_consumer_ready_target_count" + ] + rollups["ai_automation_consumer_target_context_receipt_write_count"] = ( + summary["ai_automation_consumer_target_context_receipt_write_count"] + ) + state["ai_automation_consumer_readback_ready"] = summary[ + "ai_automation_consumer_readback_ready" + ] + state["ai_automation_consumer_runtime_target_write_performed"] = summary[ + "ai_automation_consumer_runtime_target_write_performed" + ] + + apply_ai_automation_node_receipts(payload) + + def _apply_ai_automation_node_receipts(payload: dict[str, Any]) -> None: summary = _dict(payload.setdefault("summary", {})) rollups = _dict(payload.setdefault("rollups", {})) @@ -3559,6 +3697,66 @@ def _apply_ai_automation_node_receipts(payload: dict[str, Any]) -> None: summary.get("ai_loop_current_blocker_queue_readback_normalizer_field_ids") ) log_grouping_keys = _strings(summary.get("ai_loop_log_source_grouping_keys")) + executor_metadata_tagging_ready = bool( + summary.get("ai_automation_executor_metadata_only_log_source_tagging_ready") + is True + ) + executor_log_grouping_key_count = _int( + summary.get("ai_automation_executor_log_source_grouping_key_count") + ) + executor_dispatch_ready = bool( + summary.get("ai_automation_executor_controlled_dispatch_ready") is True + ) + executor_runtime_dispatch_performed = bool( + summary.get("ai_automation_executor_runtime_dispatch_performed") is True + ) + verifier_dry_run_ready = bool( + summary.get("ai_automation_post_write_verifier_dry_run_ready") is True + ) + verifier_passed_node_count = _int( + summary.get("ai_automation_post_write_verifier_passed_node_count") + ) + consumer_readback_ready = bool( + summary.get("ai_automation_consumer_readback_ready") is True + ) + consumer_runtime_target_write_performed = bool( + summary.get("ai_automation_consumer_runtime_target_write_performed") is True + ) + consumer_ready_target_count = _int( + summary.get("ai_automation_consumer_ready_target_count") + ) + consumer_context_receipt_write_count = _int( + summary.get("ai_automation_consumer_target_context_receipt_write_count") + ) + consumer_post_apply_verifier_ref_count = _int( + summary.get("ai_automation_consumer_post_apply_verifier_ref_count") + ) + consumer_context_counts = _dict( + summary.get("ai_automation_consumer_context_receipt_counts") + ) + consumer_apply_route = str(summary.get("ai_automation_consumer_apply_route") or "") + normalizer_ready = bool( + queue_fields + or log_grouping_keys + or executor_metadata_tagging_ready + or executor_log_grouping_key_count > 0 + ) + candidate_ready = bool(safe_next_action_id or executor_dispatch_ready) + execution_boundary_ready = bool( + consumer_runtime_target_write_performed + or executor_runtime_dispatch_performed + or executor_dispatch_ready + ) + post_verifier_ready = bool( + receipt_outputs + or verifier_dry_run_ready + or consumer_post_apply_verifier_ref_count > 0 + ) + learning_writeback_ready = bool( + consumer_readback_ready + and consumer_context_receipt_write_count >= consumer_ready_target_count + and consumer_ready_target_count > 0 + ) base_refs = [ "awoooi-priority-work-order-readback", @@ -3583,13 +3781,22 @@ def _apply_ai_automation_node_receipts(payload: dict[str, Any]) -> None: { "node_id": "normalizer_log_tags", "node_label": "Normalizer / LOG tags", - "node_status": "ready" if queue_fields or log_grouping_keys else "queued", + "node_status": "ready" if normalizer_ready else "queued", "work_item_id": "CIR-P0-LOG-002", "lane_id": "log_tagging_classification_clustering", - "evidence_refs": [*queue_fields[:4], *log_grouping_keys[:4]], + "evidence_refs": [ + *queue_fields[:4], + *log_grouping_keys[:4], + f"executor_log_grouping_keys:{executor_log_grouping_key_count}", + f"metadata_only_tagging_ready:{executor_metadata_tagging_ready}", + ], "verifier_result": "queue_normalizer_fields_readback" if queue_fields - else "normalizer_contract_required", + else ( + "executor_log_source_tagging_readback" + if normalizer_ready + else "normalizer_contract_required" + ), "next_action": "extend tag taxonomy to Telegram, CPU, backup, freshness", }, { @@ -3605,13 +3812,21 @@ def _apply_ai_automation_node_receipts(payload: dict[str, Any]) -> None: { "node_id": "candidate_action", "node_label": "Candidate", - "node_status": "ready" if safe_next_action_id else "queued", + "node_status": "ready" if candidate_ready else "queued", "work_item_id": "CIR-P0-AILOOP-002", "lane_id": "p0_runtime_lane", - "evidence_refs": [safe_next_action_id, safe_next_action_stage], + "evidence_refs": [ + safe_next_action_id, + safe_next_action_stage, + f"controlled_executor_dispatch_ready:{executor_dispatch_ready}", + ], "verifier_result": "safe_next_action_readback" if safe_next_action_id - else "candidate_action_required", + else ( + "controlled_executor_dispatch_ready" + if executor_dispatch_ready + else "candidate_action_required" + ), "next_action": "emit candidate action with target selector and dry-run", }, { @@ -3627,34 +3842,78 @@ def _apply_ai_automation_node_receipts(payload: dict[str, Any]) -> None: { "node_id": "execution_boundary", "node_label": "Execution Boundary", - "node_status": "queued", + "node_status": "ready" if execution_boundary_ready else "queued", "work_item_id": "CIR-P0-AILOOP-002", "lane_id": "telegram_alert_ai_automation", - "evidence_refs": ["metadata_only", "no_secret_no_runtime_write"], - "verifier_result": "execution_receipt_required", + "evidence_refs": [ + "metadata_only", + "no_secret_no_runtime_secret", + f"consumer_ready_targets:{consumer_ready_target_count}", + f"context_receipts:{consumer_context_receipt_write_count}", + f"consumer_apply_route:{consumer_apply_route}", + ], + "verifier_result": "controlled_consumer_context_receipt_readback" + if consumer_runtime_target_write_performed + else ( + "controlled_executor_dispatch_ready" + if executor_dispatch_ready + else "execution_receipt_required" + ), "next_action": "attach Telegram alert receipt and controlled executor result", + "telegram_receipt_ref": ( + f"controlled_consumer_readback:{consumer_context_receipt_write_count}" + if consumer_runtime_target_write_performed + else "metadata_only" + ), }, { "node_id": "post_verifier", "node_label": "Verifier", - "node_status": "ready" if receipt_outputs else "queued", + "node_status": "ready" if post_verifier_ready else "queued", "work_item_id": "CIR-P0-AILOOP-002", "lane_id": "p0_runtime_lane", - "evidence_refs": receipt_outputs[:4], + "evidence_refs": [ + *receipt_outputs[:4], + f"post_write_verifier_nodes:{verifier_passed_node_count}", + f"consumer_post_apply_verifier_refs:{consumer_post_apply_verifier_ref_count}", + ], "verifier_result": "post_verifier_receipt_readback" if receipt_outputs - else "post_verifier_required", + else ( + "post_write_verifier_dry_run_ready" + if verifier_dry_run_ready + else "post_verifier_required" + ), "next_action": "read back route/API/UI/verifier output before closing", }, { "node_id": "learning_writeback", "node_label": "Learning / Writeback", - "node_status": "queued", + "node_status": "ready" if learning_writeback_ready else "queued", "work_item_id": "CIR-P1-KM-002", "lane_id": "km_rag_mcp_playbook_integration", - "evidence_refs": ["KM", "RAG", "MCP", "PlayBook trust"], - "verifier_result": "writeback_receipt_required", + "evidence_refs": [ + f"context_receipts:{consumer_context_receipt_write_count}", + f"km_context_receipt:{_int(consumer_context_counts.get('km'))}", + f"rag_context_receipt:{_int(consumer_context_counts.get('rag'))}", + f"playbook_context_receipt:{_int(consumer_context_counts.get('playbook'))}", + f"mcp_context_receipt:{_int(consumer_context_counts.get('mcp'))}", + ], + "verifier_result": "consumer_context_receipt_writeback_ready" + if learning_writeback_ready + else "writeback_receipt_required", "next_action": "write KM/RAG/MCP/PlayBook trust refs after verifier", + "km_writeback_ref": ( + f"context_receipt:km={_int(consumer_context_counts.get('km'))}" + if learning_writeback_ready + else "queued" + ), + "playbook_trust_writeback_ref": ( + "context_receipt:playbook=" + f"{_int(consumer_context_counts.get('playbook'))}" + if learning_writeback_ready + else "queued" + ), }, ] diff --git a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py index baa9c1b22..a7d4ab697 100644 --- a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py +++ b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py @@ -14,7 +14,11 @@ from src.services import awoooi_priority_work_order_readback as priority_service from src.services.ai_agent_log_controlled_writeback_executor_readback import ( load_latest_ai_agent_log_controlled_writeback_executor_readback, ) +from src.services.ai_agent_log_post_write_verifier_dry_run import ( + load_latest_ai_agent_log_post_write_verifier_dry_run, +) from src.services.awoooi_priority_work_order_readback import ( + apply_ai_automation_live_closure_readbacks, apply_ai_loop_current_blocker_execution_queue, apply_controlled_cd_lane_live_metric_readback, apply_harbor_registry_controlled_recovery_preflight, @@ -458,6 +462,54 @@ def test_awoooi_priority_work_order_readback_loader_returns_mainline_order(): ] +def test_ai_automation_live_closure_readbacks_close_node_receipts(): + payload = load_latest_awoooi_priority_work_order_readback() + executor = load_latest_ai_agent_log_controlled_writeback_executor_readback() + verifier = load_latest_ai_agent_log_post_write_verifier_dry_run() + + apply_ai_automation_live_closure_readbacks( + payload, + executor_readback=executor, + verifier_readback=verifier, + consumer_readback=_consumer_readback_ready(), + ) + + receipts_by_id = { + item["node_id"]: item for item in payload["ai_automation_node_receipts"] + } + assert payload["summary"]["ai_automation_node_receipt_count"] == 8 + assert payload["summary"]["ai_automation_node_receipt_ready_count"] == 8 + assert payload["summary"]["ai_automation_node_receipt_queued_count"] == 0 + assert payload["summary"]["ai_automation_node_receipt_blocked_count"] == 0 + assert receipts_by_id["normalizer_log_tags"]["node_status"] == "ready" + assert receipts_by_id["candidate_action"]["node_status"] == "ready" + assert receipts_by_id["execution_boundary"]["node_status"] == "ready" + assert receipts_by_id["post_verifier"]["node_status"] == "ready" + assert receipts_by_id["learning_writeback"]["node_status"] == "ready" + assert receipts_by_id["execution_boundary"]["verifier_result"] == ( + "controlled_consumer_context_receipt_readback" + ) + assert receipts_by_id["post_verifier"]["verifier_result"] == ( + "post_write_verifier_dry_run_ready" + ) + assert receipts_by_id["learning_writeback"]["verifier_result"] == ( + "consumer_context_receipt_writeback_ready" + ) + assert receipts_by_id["learning_writeback"]["km_writeback_ref"] == ( + "context_receipt:km=1" + ) + assert receipts_by_id["learning_writeback"]["playbook_trust_writeback_ref"] == ( + "context_receipt:playbook=1" + ) + assert payload["summary"][ + "ai_automation_consumer_runtime_target_write_performed" + ] is True + assert payload["summary"]["ai_automation_consumer_ready_target_count"] == 6 + assert payload["summary"][ + "ai_automation_consumer_target_context_receipt_write_count" + ] == 6 + + def test_awoooi_priority_work_order_readback_endpoint_returns_snapshot( monkeypatch: pytest.MonkeyPatch, ): @@ -476,6 +528,11 @@ def test_awoooi_priority_work_order_readback_endpoint_returns_snapshot( "load_controlled_cd_lane_live_metric_readback", _controlled_cd_lane_live_metrics, ) + monkeypatch.setattr( + agents, + "load_latest_ai_agent_log_controlled_writeback_consumer_readback", + _consumer_readback_ready_async, + ) app = FastAPI() app.include_router(router, prefix="/api/v1") client = TestClient(app) @@ -545,8 +602,9 @@ def test_awoooi_priority_work_order_readback_endpoint_returns_snapshot( "ai_automation_node_receipt_v1" ) assert data["summary"]["ai_automation_node_receipt_count"] == 8 - assert data["summary"]["ai_automation_node_receipt_ready_count"] >= 3 - assert data["summary"]["ai_automation_node_receipt_queued_count"] >= 1 + assert data["summary"]["ai_automation_node_receipt_ready_count"] == 8 + assert data["summary"]["ai_automation_node_receipt_queued_count"] == 0 + assert data["summary"]["ai_automation_node_receipt_blocked_count"] == 0 assert [item["node_id"] for item in data["ai_automation_node_receipts"]] == [ "sensor_evidence", "normalizer_log_tags", @@ -566,6 +624,12 @@ def test_awoooi_priority_work_order_readback_endpoint_returns_snapshot( assert endpoint_receipts["execution_boundary"]["lane_id"] == ( "telegram_alert_ai_automation" ) + assert endpoint_receipts["execution_boundary"]["node_status"] == "ready" + assert endpoint_receipts["post_verifier"]["node_status"] == "ready" + assert endpoint_receipts["learning_writeback"]["node_status"] == "ready" + assert endpoint_receipts["learning_writeback"]["km_writeback_ref"] == ( + "context_receipt:km=1" + ) def test_controlled_cd_lane_live_metric_readback_falls_back_to_prometheus( @@ -1530,6 +1594,58 @@ def _controlled_cd_lane_live_metrics() -> dict: } +async def _consumer_readback_ready_async() -> dict: + return _consumer_readback_ready() + + +def _consumer_readback_ready() -> dict: + return { + "schema_version": "ai_agent_log_controlled_writeback_consumer_readback_v1", + "status": "controlled_writeback_consumer_readback_ready", + "controlled_consume": { + "mode": "controlled_consumer_readback_ready", + "controlled_consume_allowed": True, + "owner_review_required_for_low_medium_high": False, + "critical_break_glass_required": True, + "target_selector_required": True, + "source_of_truth_diff_required": True, + "check_mode_required": True, + "rollback_required": True, + "post_apply_verifier_required": True, + "runtime_target_write_performed": True, + "consumer_apply_route": ( + "/api/v1/agents/agent-log-controlled-writeback-consumer-apply" + ), + }, + "rollups": { + "target_count": 6, + "dispatch_ledger_row_count": 6, + "consumer_apply_receipt_row_count": 6, + "consumer_binding_count": 6, + "ready_consumer_binding_count": 6, + "ready_target_count": 6, + "metadata_only_receipt_count": 6, + "post_apply_verifier_ref_count": 12, + "controlled_consumer_readback_ready": True, + "km_consumer_binding_count": 1, + "rag_consumer_binding_count": 1, + "playbook_consumer_binding_count": 1, + "mcp_consumer_binding_count": 1, + "verifier_consumer_binding_count": 1, + "ai_agent_consumer_binding_count": 1, + "target_context_receipt_write_count": 6, + "km_context_receipt_write_count": 1, + "rag_context_receipt_write_count": 1, + "playbook_context_receipt_write_count": 1, + "mcp_context_receipt_write_count": 1, + "verifier_context_receipt_write_count": 1, + "ai_agent_context_receipt_write_count": 1, + "runtime_target_write_performed": True, + }, + "active_blockers": [], + } + + def _stockplatform_runtime_blocked() -> dict: return { "schema_version": "stockplatform_public_api_runtime_readback_v1",