From c1337fea77fa811ce47f4006b8b9a4417ca643f8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 29 Jun 2026 20:24:19 +0800 Subject: [PATCH] feat(api): expose agent log intelligence readback --- apps/api/src/api/v1/agents.py | 34 +++ ...t_log_intelligence_integration_readback.py | 228 ++++++++++++++++++ ...g_intelligence_integration_readback_api.py | 111 +++++++++ 3 files changed, 373 insertions(+) create mode 100644 apps/api/src/services/ai_agent_log_intelligence_integration_readback.py create mode 100644 apps/api/tests/test_ai_agent_log_intelligence_integration_readback_api.py diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index cd392c524..a020939f2 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -97,6 +97,9 @@ from src.services.ai_agent_interaction_learning_proof import ( from src.services.ai_agent_learning_writeback_approval_package import ( load_latest_ai_agent_learning_writeback_approval_package, ) +from src.services.ai_agent_log_intelligence_integration_readback import ( + load_latest_ai_agent_log_intelligence_integration_readback, +) from src.services.ai_agent_live_read_model_gate import ( load_latest_ai_agent_live_read_model_gate, ) @@ -1800,6 +1803,37 @@ async def get_agent_learning_writeback_approval_package() -> dict[str, Any]: ) from exc +@router.get( + "/agent-log-intelligence-integration-readback", + response_model=dict[str, Any], + summary="取得 AI Agent LOG / KM / RAG / MCP / PlayBook 整合 readback", + description=( + "讀取目前 API image 內已提交的 LOG intelligence source refs,彙總服務日誌、" + "分類摘要、KM/RAG、MCP audit、PlayBook learning 與 AI Agent 決策 runtime 的串接面。" + "此端點不查 live log、不寫 KM、不寫 RAG index、不更新 PlayBook trust、不呼叫 MCP tool、" + "不執行修復、不觸發 workflow、不讀 secret、不讀 raw session / SQLite、不呼叫 GitHub。" + ), +) +async def get_agent_log_intelligence_integration_readback() -> dict[str, Any]: + """Return the read-only LOG intelligence integration matrix.""" + try: + payload = await asyncio.to_thread( + load_latest_ai_agent_log_intelligence_integration_readback + ) + return redact_public_lan_topology(payload) + except FileNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(exc), + ) from exc + except (json.JSONDecodeError, ValueError) as exc: + logger.error("ai_agent_log_intelligence_integration_readback_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="AI Agent LOG / KM / RAG / MCP / PlayBook 整合 readback 無效", + ) from exc + + @router.get( "/agent-telegram-receipt-approval-package", response_model=dict[str, Any], diff --git a/apps/api/src/services/ai_agent_log_intelligence_integration_readback.py b/apps/api/src/services/ai_agent_log_intelligence_integration_readback.py new file mode 100644 index 000000000..581a0fbc4 --- /dev/null +++ b/apps/api/src/services/ai_agent_log_intelligence_integration_readback.py @@ -0,0 +1,228 @@ +"""AI Agent log intelligence integration readback. + +This service exposes a source-of-truth map for LOG -> KM/RAG/MCP/PlayBook +automation. It only inspects committed source files packaged with the API image; +it does not query live logs, read secrets, write KM, update PlayBook trust, +call MCP tools, trigger workflows, or execute runtime repairs. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from src.services.snapshot_paths import resolve_repo_root + +_SCHEMA_VERSION = "ai_agent_log_intelligence_integration_readback_v1" +_DEFAULT_REPO_ROOT = resolve_repo_root(Path(__file__)) + +_LANES: tuple[dict[str, Any], ...] = ( + { + "lane_id": "structured_service_log_collection", + "title": "服務日誌與可觀測性來源", + "source_kind": "logs_metrics_traces", + "integration_target": "diagnosis_context", + "required_refs": ( + "apps/api/src/core/logging.py", + "apps/api/src/core/deep_linking.py", + "apps/api/src/services/diagnosis_aggregator.py", + ), + "required_labels": ("project_id", "service", "environment", "trace_id", "severity"), + }, + { + "lane_id": "log_classification_summary", + "title": "Log 分類、摘要與異常訊號", + "source_kind": "log_classification", + "integration_target": "agent_evidence_packet", + "required_refs": ( + "apps/api/src/services/log_anomaly_detector.py", + "apps/api/src/services/log_summary_service.py", + "apps/api/src/services/diagnosis_aggregator.py", + ), + "required_labels": ("signal_kind", "severity", "service", "package", "tool"), + }, + { + "lane_id": "km_rag_consumption", + "title": "KM / RAG 證據消費", + "source_kind": "knowledge_memory", + "integration_target": "rag_context", + "required_refs": ( + "apps/api/src/services/knowledge_extractor_service.py", + "apps/api/src/services/knowledge_service.py", + "apps/api/src/services/knowledge_rag_service.py", + "apps/api/src/services/rag_service.py", + "apps/api/src/services/graph_rag.py", + ), + "required_labels": ("project_id", "product", "service", "incident_id", "evidence_ref"), + }, + { + "lane_id": "playbook_learning_loop", + "title": "PlayBook 匹配、RAG 與 trust 學習", + "source_kind": "playbook_learning", + "integration_target": "playbook_candidate_and_trust_gate", + "required_refs": ( + "apps/api/src/services/playbook_rag.py", + "apps/api/src/services/playbook_match_resolver.py", + "apps/api/src/services/playbook_embedding_service.py", + "apps/api/src/services/playbook_evolver.py", + "apps/api/src/services/ai_agent_matched_playbook_learning_gap.py", + ), + "required_labels": ("playbook_id", "incident_id", "service", "risk_tier", "verifier_id"), + }, + { + "lane_id": "mcp_tool_audit_context", + "title": "MCP 工具、稽核上下文與工具註冊", + "source_kind": "mcp_tools", + "integration_target": "controlled_tool_context", + "required_refs": ( + "apps/api/src/plugins/mcp/mcp_bridge.py", + "apps/api/src/services/mcp_audit_context.py", + "apps/api/src/services/mcp_audit_service.py", + "apps/api/src/services/mcp_tool_registry.py", + ), + "required_labels": ("mcp_server", "tool", "agent_run_id", "gateway_path", "redaction_state"), + }, + { + "lane_id": "agent_decision_runtime_context", + "title": "AI Agent 決策與結果捕獲", + "source_kind": "agent_runtime", + "integration_target": "controlled_decision_runtime", + "required_refs": ( + "apps/api/src/services/ai_agent_autonomous_runtime_control.py", + "apps/api/src/services/ai_agent_task_result_audit_trail.py", + "apps/api/src/services/ai_agent_result_capture_writer_dry_run_readback.py", + "apps/api/src/services/ai_agent_result_capture_no_write_readback.py", + ), + "required_labels": ("agent_run_id", "decision_id", "risk_tier", "source_system", "verifier_id"), + }, + { + "lane_id": "post_apply_verifier_feedback", + "title": "Verifier 與 post-apply feedback", + "source_kind": "verification_feedback", + "integration_target": "learning_writeback_candidate", + "required_refs": ( + "apps/api/src/services/ai_agent_runtime_verifier_evidence_review.py", + "apps/api/src/services/ai_agent_post_write_verifier_package.py", + "apps/api/src/services/ai_agent_result_capture_post_release_verifier_rollback_gate.py", + ), + "required_labels": ("verifier_id", "rollback_ref", "service", "environment", "decision_id"), + }, +) + +_LABEL_TAXONOMY: tuple[dict[str, Any], ...] = ( + { + "label_group": "ownership", + "required_fields": ("project_id", "product", "service", "owner_lane"), + "purpose": "把所有 log / event 綁回專案、產品、服務與 owner lane。", + }, + { + "label_group": "runtime_surface", + "required_fields": ("environment", "host", "package", "tool", "source_system"), + "purpose": "把網站、服務、套件、工具與主機來源分群。", + }, + { + "label_group": "correlation", + "required_fields": ("trace_id", "incident_id", "agent_run_id", "decision_id"), + "purpose": "把 log、告警、AI run、AwoooP work item 與 verifier 串成同一條證據鏈。", + }, + { + "label_group": "learning", + "required_fields": ("playbook_id", "verifier_id", "risk_tier", "redaction_state"), + "purpose": "讓 KM/RAG/PlayBook trust writeback 能判斷是否可學習。", + }, +) + + +def load_latest_ai_agent_log_intelligence_integration_readback( + repo_root: Path | None = None, +) -> dict[str, Any]: + """Build the latest LOG -> AI automation integration readback.""" + root = repo_root or _DEFAULT_REPO_ROOT + lanes = [_build_lane(root, lane) for lane in _LANES] + ready_lane_count = sum(1 for lane in lanes if lane["status"] == "source_refs_present") + missing_lane_count = len(lanes) - ready_lane_count + label_field_count = len( + {field for group in _LABEL_TAXONOMY for field in group["required_fields"]} + ) + active_blockers = [] + if missing_lane_count: + active_blockers.append("committed_source_refs_missing") + active_blockers.append("runtime_e2e_log_sample_readback_missing") + + return { + "schema_version": _SCHEMA_VERSION, + "priority": "P1-LOG-KM-RAG-MCP-PLAYBOOK", + "scope": "ai_agent_log_intelligence_integration", + "status": ( + "controlled_apply_ready_missing_runtime_e2e_log_sample" + if not missing_lane_count + else "blocked_missing_committed_source_refs" + ), + "readback": { + "workplan_id": "P1-LOG-INTELLIGENCE", + "workplan_title": "所有服務日誌貼標並串接 KM / RAG / MCP / PlayBook / AI Agent", + "repo_root_ref": str(root), + "safe_next_step": ( + "add_runtime_log_sample_verifier_then_write_trusted_KM_RAG_PlayBook_feedback_receipt" + ), + }, + "integration_lanes": lanes, + "label_taxonomy": list(_LABEL_TAXONOMY), + "rollups": { + "lane_count": len(lanes), + "ready_lane_count": ready_lane_count, + "missing_lane_count": missing_lane_count, + "source_ref_count": sum(len(lane["evidence_refs"]) for lane in lanes), + "missing_source_ref_count": sum(len(lane["missing_refs"]) for lane in lanes), + "readiness_percent": _percent(ready_lane_count / max(len(lanes), 1) * 100), + "label_group_count": len(_LABEL_TAXONOMY), + "required_label_field_count": label_field_count, + "runtime_e2e_log_sample_readback_present": False, + "km_rag_playbook_trust_writeback_authorized": False, + "mcp_tool_execution_authorized": False, + }, + "active_blockers": active_blockers, + "operation_boundaries": { + "read_only_api_allowed": True, + "live_log_query_performed": False, + "km_write_performed": False, + "rag_index_write_performed": False, + "playbook_trust_write_performed": False, + "mcp_tool_call_performed": False, + "runtime_repair_performed": False, + "workflow_trigger_performed": False, + "secret_value_collection_allowed": False, + "raw_session_or_sqlite_read_allowed": False, + "github_api_used": False, + }, + } + + +def _build_lane(root: Path, lane: dict[str, Any]) -> dict[str, Any]: + evidence_refs = [str(ref) for ref in lane["required_refs"]] + missing_refs = [ref for ref in evidence_refs if not _ref_exists(root, ref)] + return { + "lane_id": lane["lane_id"], + "title": lane["title"], + "source_kind": lane["source_kind"], + "integration_target": lane["integration_target"], + "status": "source_refs_present" if not missing_refs else "missing_source_refs", + "required_labels": list(lane["required_labels"]), + "evidence_refs": evidence_refs, + "missing_refs": missing_refs, + "controlled_apply_next_step": "attach_runtime_sample_and_post_apply_verifier", + "runtime_write_enabled": False, + "secret_value_collection_allowed": False, + } + + +def _ref_exists(root: Path, ref: str) -> bool: + if (root / ref).exists(): + return True + if ref.startswith("apps/api/"): + return (root / ref.removeprefix("apps/api/")).exists() + return False + + +def _percent(value: Any) -> int: + return max(0, min(100, round(float(value or 0)))) diff --git a/apps/api/tests/test_ai_agent_log_intelligence_integration_readback_api.py b/apps/api/tests/test_ai_agent_log_intelligence_integration_readback_api.py new file mode 100644 index 000000000..5843d9087 --- /dev/null +++ b/apps/api/tests/test_ai_agent_log_intelligence_integration_readback_api.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.api.v1.agents import router +from src.services.ai_agent_log_intelligence_integration_readback import ( + _ref_exists, + load_latest_ai_agent_log_intelligence_integration_readback, +) + + +def test_ai_agent_log_intelligence_integration_loader_maps_all_lanes(): + payload = load_latest_ai_agent_log_intelligence_integration_readback() + + _assert_log_intelligence_payload(payload) + + +def test_ai_agent_log_intelligence_integration_endpoint_returns_readback(): + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.get( + "/api/v1/agents/agent-log-intelligence-integration-readback" + ) + + assert response.status_code == 200 + _assert_log_intelligence_payload(response.json()) + + +def test_log_intelligence_ref_exists_supports_api_container_layout(tmp_path): + container_ref = tmp_path / "src" / "services" / "diagnosis_aggregator.py" + container_ref.parent.mkdir(parents=True) + container_ref.write_text("# packaged API source\n", encoding="utf-8") + + assert _ref_exists( + tmp_path, + "apps/api/src/services/diagnosis_aggregator.py", + ) + + +def _assert_log_intelligence_payload(payload: dict): + assert ( + payload["schema_version"] + == "ai_agent_log_intelligence_integration_readback_v1" + ) + assert payload["priority"] == "P1-LOG-KM-RAG-MCP-PLAYBOOK" + assert payload["status"] == "controlled_apply_ready_missing_runtime_e2e_log_sample" + assert payload["rollups"]["lane_count"] == 7 + assert payload["rollups"]["ready_lane_count"] == 7 + assert payload["rollups"]["missing_lane_count"] == 0 + assert payload["rollups"]["readiness_percent"] == 100 + assert payload["rollups"]["runtime_e2e_log_sample_readback_present"] is False + assert payload["rollups"]["km_rag_playbook_trust_writeback_authorized"] is False + assert payload["rollups"]["mcp_tool_execution_authorized"] is False + assert "runtime_e2e_log_sample_readback_missing" in payload["active_blockers"] + + lanes = {lane["lane_id"]: lane for lane in payload["integration_lanes"]} + assert set(lanes) == { + "structured_service_log_collection", + "log_classification_summary", + "km_rag_consumption", + "playbook_learning_loop", + "mcp_tool_audit_context", + "agent_decision_runtime_context", + "post_apply_verifier_feedback", + } + assert "apps/api/src/services/diagnosis_aggregator.py" in lanes[ + "structured_service_log_collection" + ]["evidence_refs"] + assert "apps/api/src/services/knowledge_rag_service.py" in lanes[ + "km_rag_consumption" + ]["evidence_refs"] + assert "apps/api/src/services/playbook_rag.py" in lanes[ + "playbook_learning_loop" + ]["evidence_refs"] + assert "apps/api/src/plugins/mcp/mcp_bridge.py" in lanes[ + "mcp_tool_audit_context" + ]["evidence_refs"] + assert all(lane["runtime_write_enabled"] is False for lane in lanes.values()) + assert all(not lane["missing_refs"] for lane in lanes.values()) + + label_groups = { + group["label_group"]: set(group["required_fields"]) + for group in payload["label_taxonomy"] + } + assert {"project_id", "product", "service"}.issubset( + label_groups["ownership"] + ) + assert {"package", "tool", "source_system"}.issubset( + label_groups["runtime_surface"] + ) + assert {"trace_id", "incident_id", "agent_run_id"}.issubset( + label_groups["correlation"] + ) + assert {"playbook_id", "verifier_id", "risk_tier"}.issubset( + label_groups["learning"] + ) + + boundaries = payload["operation_boundaries"] + assert boundaries["read_only_api_allowed"] is True + assert boundaries["live_log_query_performed"] is False + assert boundaries["km_write_performed"] is False + assert boundaries["rag_index_write_performed"] is False + assert boundaries["playbook_trust_write_performed"] is False + assert boundaries["mcp_tool_call_performed"] is False + assert boundaries["runtime_repair_performed"] is False + assert boundaries["secret_value_collection_allowed"] is False + assert boundaries["raw_session_or_sqlite_read_allowed"] is False + assert boundaries["github_api_used"] is False