feat(adr-080): Phase 0 防護欄建立 — AI 自主化飛輪啟動
- docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md (1456 行,§0-§8 全填完:42-cell 戰術矩陣、7 Phase 計畫、7 ADR 摘要、 15 KPI、21 Feature Flags、10 風險場景) - docs/adr/ADR-080-ai-autonomy-flywheel-overview.md (7 Phase 結構 + 4 北極星 + 7 架構師 Review Gates + Phase 退出條件) - apps/api/src/core/feature_flags.py (AIOpsFeatureFlags: P1~P6 總開關全 False + 15 細粒度子開關 is_phase_enabled() / is_sub_flag_enabled() + bool cast 安全) - apps/api/src/jobs/__init__.py + baseline_snapshot.py (Phase 0 基線快照 Job:MCP calls / Playbook confidence / general 比例 / learning loop rate / auto_repair — 寫入 aiops:baseline:latest) - apps/api/tests/test_feature_flags.py (21 tests — 全綠) - docs/HARD_RULES.md → v1.9 (新增 Phase 退出條件鐵律:禁止未過 exit conditions 宣告 Phase 完成) - CLAUDE.md 防失憶閘門 1:強制讀 MASTER §0 Session Resume Protocol Gate 0 Pass — 21/21 tests green Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
240
apps/api/src/core/feature_flags.py
Normal file
240
apps/api/src/core/feature_flags.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""
|
||||
AWOOOI AIOps Feature Flags
|
||||
==========================
|
||||
AI 自主化飛輪 Phase 0-6 功能開關
|
||||
|
||||
ADR-080: AI 自主化飛輪總綱
|
||||
MASTER: docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md
|
||||
|
||||
安全規則:
|
||||
- 所有 flag 預設 False — 任何 Phase 必須明確開啟才生效
|
||||
- Phase 總開關 = False 時,該 Phase 所有子開關均視為 False
|
||||
- 自我降級後 (D6) 不得自動反向升級,升級必須人工設定 env var
|
||||
|
||||
回滾方式:
|
||||
kubectl set env deployment/awoooi-api AIOPS_P1_ENABLED=false
|
||||
# 或修改 .env 後重部署
|
||||
|
||||
2026-04-15 ogt: Phase 0 — 初始建立,ADR-080 批准後啟用
|
||||
"""
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class AIOpsFeatureFlags(BaseSettings):
|
||||
"""
|
||||
AI 自主化飛輪 Feature Flag 集合
|
||||
|
||||
每個 Phase 一個總開關 + 細粒度子開關。
|
||||
讀取順序:環境變數 > .env 檔 > 預設值(全 False)。
|
||||
"""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
case_sensitive=True,
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# Phase 總開關(Phase N 退出條件達到後才設 True)
|
||||
# ==========================================================================
|
||||
|
||||
AIOPS_P1_ENABLED: bool = Field(
|
||||
default=False,
|
||||
description="Phase 1 感官縱深:PreDecisionInvestigator + EvidenceSnapshot + PostExecutionVerifier",
|
||||
)
|
||||
AIOPS_P2_ENABLED: bool = Field(
|
||||
default=False,
|
||||
description="Phase 2 多 Agent 協作:5 角色全部上線(Diagnostician/Solver/Reviewer/Critic/Coordinator)",
|
||||
)
|
||||
AIOPS_P3_ENABLED: bool = Field(
|
||||
default=False,
|
||||
description="Phase 3 學習閉環重建:3 根因修復 + EWMA + Evolver + Fine-tune pipeline",
|
||||
)
|
||||
AIOPS_P4_ENABLED: bool = Field(
|
||||
default=False,
|
||||
description="Phase 4 動態異常偵測:Holt-Winters + Drain3 + Prophet + 主動巡檢",
|
||||
)
|
||||
AIOPS_P5_ENABLED: bool = Field(
|
||||
default=False,
|
||||
description="Phase 5 修復抽象化:Declarative + Blast Radius 四級分控 + GitOps PR",
|
||||
)
|
||||
AIOPS_P6_ENABLED: bool = Field(
|
||||
default=False,
|
||||
description="Phase 6 自我治理閉環:SLO + Trust Drift + KB Rot + 離線回放 + 自我降級",
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# Phase 1 細粒度子開關
|
||||
# ==========================================================================
|
||||
|
||||
AIOPS_P1_PRE_DECISION_INVESTIGATOR: bool = Field(
|
||||
default=False,
|
||||
description="P1: PreDecisionInvestigator 是否在決策前執行 MCP 感官蒐集(可獨立關閉)",
|
||||
)
|
||||
AIOPS_P1_POST_EXECUTION_VERIFIER: bool = Field(
|
||||
default=False,
|
||||
description="P1: PostExecutionVerifier 是否在每次執行後驗證狀態",
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# Phase 2 細粒度子開關
|
||||
# ==========================================================================
|
||||
|
||||
AIOPS_P2_CRITIC_ENABLED: bool = Field(
|
||||
default=False,
|
||||
description="P2: Critic Agent 是否啟用辯證挑戰(關閉可降低延遲但失去質疑機制)",
|
||||
)
|
||||
AIOPS_P2_AGENT_TIMEOUT_SEC: int = Field(
|
||||
default=5,
|
||||
description="P2: 單 Agent 熔斷閾值(秒),超時則 Coordinator 降級處理",
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# Phase 3 細粒度子開關
|
||||
# ==========================================================================
|
||||
|
||||
AIOPS_P3_FINETUNE_EXPORT: bool = Field(
|
||||
default=False,
|
||||
description="P3: Fine-tune JSONL 每週匯出到 MinIO 是否執行",
|
||||
)
|
||||
AIOPS_P3_EVOLVER_ENABLED: bool = Field(
|
||||
default=False,
|
||||
description="P3: Evolver Agent 是否執行 Playbook 自動合併與封存",
|
||||
)
|
||||
AIOPS_P3_KNOWLEDGE_DECAY: bool = Field(
|
||||
default=False,
|
||||
description="P3: 30 天知識遺忘 job 是否執行(標 decayed,降到 cold index)",
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# Phase 4 細粒度子開關
|
||||
# ==========================================================================
|
||||
|
||||
AIOPS_P4_DYNAMIC_BASELINE: bool = Field(
|
||||
default=False,
|
||||
description="P4: Holt-Winters 動態基線服務是否啟用",
|
||||
)
|
||||
AIOPS_P4_LOG_ANOMALY: bool = Field(
|
||||
default=False,
|
||||
description="P4: Drain3 日誌異常偵測是否啟用",
|
||||
)
|
||||
AIOPS_P4_TREND_PREDICTOR: bool = Field(
|
||||
default=False,
|
||||
description="P4: Prophet 趨勢預測是否啟用(預測 4h 內超閾值風險)",
|
||||
)
|
||||
AIOPS_P4_PROACTIVE_INSPECTOR: bool = Field(
|
||||
default=False,
|
||||
description="P4: 主動巡檢每 5min 是否執行",
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# Phase 5 細粒度子開關
|
||||
# ==========================================================================
|
||||
|
||||
AIOPS_P5_BLAST_RADIUS_CHECK: bool = Field(
|
||||
default=False,
|
||||
description="P5: Blast Radius 評估是否執行(False = 全部視為低風險自動執行,危險)",
|
||||
)
|
||||
AIOPS_P5_GITOPS_PR: bool = Field(
|
||||
default=False,
|
||||
description="P5: 高風險修復(Blast Radius > 50)是否走 GitOps Gitea PR 流程",
|
||||
)
|
||||
AIOPS_P5_DRY_RUN_ENFORCED: bool = Field(
|
||||
default=False,
|
||||
description="P5: Declarative apply 前是否強制 dry-run(False = 跳過 dry-run,危險)",
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# Phase 6 細粒度子開關
|
||||
# ==========================================================================
|
||||
|
||||
AIOPS_P6_SELF_DEMOTION: bool = Field(
|
||||
default=False,
|
||||
description="P6: 自我降級邏輯是否啟用(SLO 違反 → 自動提高信心閾值)",
|
||||
)
|
||||
AIOPS_P6_OFFLINE_REPLAY: bool = Field(
|
||||
default=False,
|
||||
description="P6: 週度離線回放 100 案是否執行",
|
||||
)
|
||||
AIOPS_P6_KB_ROT_CLEANER: bool = Field(
|
||||
default=False,
|
||||
description="P6: 月度 KB 腐爛清理 job 是否執行",
|
||||
)
|
||||
AIOPS_P6_TRUST_DRIFT_DETECTOR: bool = Field(
|
||||
default=False,
|
||||
description="P6: Playbook trust 分布漂移偵測是否啟用",
|
||||
)
|
||||
|
||||
def is_phase_enabled(self, phase: int) -> bool:
|
||||
"""
|
||||
檢查指定 Phase 的總開關是否啟用。
|
||||
|
||||
Args:
|
||||
phase: Phase 編號(1-6)
|
||||
|
||||
Returns:
|
||||
bool: 該 Phase 是否開啟
|
||||
|
||||
Usage:
|
||||
if flags.is_phase_enabled(1):
|
||||
await pre_decision_investigator.investigate(...)
|
||||
"""
|
||||
phase_flags = {
|
||||
1: self.AIOPS_P1_ENABLED,
|
||||
2: self.AIOPS_P2_ENABLED,
|
||||
3: self.AIOPS_P3_ENABLED,
|
||||
4: self.AIOPS_P4_ENABLED,
|
||||
5: self.AIOPS_P5_ENABLED,
|
||||
6: self.AIOPS_P6_ENABLED,
|
||||
}
|
||||
return phase_flags.get(phase, False)
|
||||
|
||||
def is_sub_flag_enabled(self, flag_name: str) -> bool:
|
||||
"""
|
||||
檢查細粒度子開關(自動驗證父 Phase 開關)。
|
||||
|
||||
Args:
|
||||
flag_name: 子開關名稱,例如 "AIOPS_P1_PRE_DECISION_INVESTIGATOR"
|
||||
|
||||
Returns:
|
||||
bool: 子開關 AND 父 Phase 開關都為 True 才回 True
|
||||
|
||||
Usage:
|
||||
if flags.is_sub_flag_enabled("AIOPS_P1_PRE_DECISION_INVESTIGATOR"):
|
||||
...
|
||||
"""
|
||||
# 解析 Phase 編號
|
||||
parts = flag_name.split("_")
|
||||
if len(parts) < 3 or not parts[1].startswith("P"):
|
||||
return False
|
||||
|
||||
try:
|
||||
phase = int(parts[1][1:])
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
# 父 Phase 必須開啟
|
||||
if not self.is_phase_enabled(phase):
|
||||
return False
|
||||
|
||||
return bool(getattr(self, flag_name, False))
|
||||
|
||||
|
||||
# Singleton — 與 core/config.py 的 settings 相同模式
|
||||
# 使用:from src.core.feature_flags import aiops_flags
|
||||
aiops_flags = AIOpsFeatureFlags()
|
||||
|
||||
|
||||
def get_aiops_flags() -> AIOpsFeatureFlags:
|
||||
"""
|
||||
FastAPI dependency injection 用。
|
||||
|
||||
Usage:
|
||||
@router.get("/status")
|
||||
async def status(flags: AIOpsFeatureFlags = Depends(get_aiops_flags)):
|
||||
return {"p1": flags.AIOPS_P1_ENABLED}
|
||||
"""
|
||||
return aiops_flags
|
||||
15
apps/api/src/jobs/__init__.py
Normal file
15
apps/api/src/jobs/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
AWOOOI AIOps Jobs
|
||||
==================
|
||||
定時任務(非 Redis Streams Worker)
|
||||
|
||||
目前包含:
|
||||
- baseline_snapshot: Phase 0 觀測基線快照
|
||||
- knowledge_decay_job: Phase 3 30 天知識遺忘 (待建)
|
||||
- detection_feedback_writer: Phase 3 誤判告警回寫 (待建)
|
||||
- offline_replay_service: Phase 6 週度離線回放 (待建)
|
||||
- kb_rot_cleaner: Phase 6 月度 KB 腐爛清理 (待建)
|
||||
|
||||
ADR-080: AI 自主化飛輪總綱
|
||||
2026-04-15 ogt: Phase 0 — 初始建立
|
||||
"""
|
||||
338
apps/api/src/jobs/baseline_snapshot.py
Normal file
338
apps/api/src/jobs/baseline_snapshot.py
Normal file
@@ -0,0 +1,338 @@
|
||||
"""
|
||||
AWOOOI AIOps Phase 0 — 基線快照 Job
|
||||
=====================================
|
||||
拍攝 AI 自主化飛輪「啟動前現況」,作為 Phase 0→1 進展衡量基準。
|
||||
|
||||
快照涵蓋 ADR-080 診斷表中的 6 大指標:
|
||||
1. MCP 呼叫次數/24h(目標:> 0;現況預估:0)
|
||||
2. Playbook trust/confidence 分佈(目標:動態;現況:全靜態)
|
||||
3. 學習閉環觸發率(目標:≥ 99%;現況:0%,fire-and-forget)
|
||||
4. 告警分類 general 比例(目標:< 10%;現況:~ 41%)
|
||||
5. 修復動作 RESTART 比例(目標:< 40%;現況:~ 68%)
|
||||
6. 自動執行成功次數/24h(目標:> 0;現況:0)
|
||||
|
||||
儲存策略:
|
||||
- Redis Key `aiops:baseline:{timestamp_iso}` — 最新快照(TTL 永不過期)
|
||||
- Redis Key `aiops:baseline:latest` — 指向最新快照的時間戳(方便 API 讀取)
|
||||
|
||||
使用方式:
|
||||
python -m src.jobs.baseline_snapshot # 直接執行(一次性)
|
||||
await take_baseline_snapshot() # 從程式碼呼叫
|
||||
|
||||
ADR-080: AI 自主化飛輪總綱
|
||||
MASTER: docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md §5 Phase 0
|
||||
|
||||
2026-04-15 ogt + Claude Sonnet 4.6 (亞太): Phase 0 — 初始建立
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import func, select, text
|
||||
|
||||
from src.core.redis_client import get_redis
|
||||
from src.db.base import get_db_context
|
||||
from src.db.models import (
|
||||
AutoRepairExecution,
|
||||
IncidentRecord,
|
||||
KnowledgeEntryRecord,
|
||||
)
|
||||
from src.utils.timezone import now_taipei
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# Redis 鍵
|
||||
BASELINE_KEY_PREFIX = "aiops:baseline:"
|
||||
BASELINE_LATEST_KEY = "aiops:baseline:latest"
|
||||
|
||||
# Playbook Redis 前綴(同 playbook_repository.py)
|
||||
PLAYBOOK_KEY_PREFIX = "playbook:"
|
||||
|
||||
|
||||
async def take_baseline_snapshot() -> dict:
|
||||
"""
|
||||
拍攝一次完整基線快照並寫入 Redis。
|
||||
|
||||
Returns:
|
||||
dict: 快照內容(含 snapshot_at 時間戳)
|
||||
"""
|
||||
now = now_taipei()
|
||||
since_24h = now - timedelta(hours=24)
|
||||
ts_iso = now.isoformat()
|
||||
|
||||
logger.info("baseline_snapshot_start", snapshot_at=ts_iso)
|
||||
|
||||
snapshot = {
|
||||
"snapshot_at": ts_iso,
|
||||
"phase": "P0",
|
||||
"description": "AI 自主化飛輪 Phase 0 啟動前基線",
|
||||
"metrics": {},
|
||||
}
|
||||
|
||||
# ── 1. MCP 呼叫次數/24h ───────────────────────────────────────────────
|
||||
# Phase 0 時 MCP 尚未接入任何決策流程 → 預期為 0
|
||||
# Phase 1 完成後此數字應 > 0(PreDecisionInvestigator 開始呼叫)
|
||||
mcp_calls_24h = await _count_mcp_calls_24h(since_24h)
|
||||
snapshot["metrics"]["mcp_calls_24h"] = mcp_calls_24h
|
||||
|
||||
# ── 2. Playbook confidence 分佈(Redis 掃描)──────────────────────────
|
||||
playbook_stats = await _playbook_confidence_stats()
|
||||
snapshot["metrics"]["playbook"] = playbook_stats
|
||||
|
||||
# ── 3. 學習閉環觸發率 + 其他 DB 指標 ─────────────────────────────────
|
||||
db_metrics = await _db_metrics(since_24h)
|
||||
snapshot["metrics"].update(db_metrics)
|
||||
|
||||
# ── 4. 計算衍生指標 ───────────────────────────────────────────────────
|
||||
snapshot["metrics"]["learning_loop_rate"] = _compute_learning_rate(
|
||||
db_metrics.get("auto_repair_24h", 0),
|
||||
db_metrics.get("learning_writes_24h", 0),
|
||||
)
|
||||
|
||||
# ── 寫入 Redis ─────────────────────────────────────────────────────────
|
||||
await _persist_to_redis(ts_iso, snapshot)
|
||||
|
||||
logger.info(
|
||||
"baseline_snapshot_done",
|
||||
snapshot_at=ts_iso,
|
||||
mcp_calls_24h=mcp_calls_24h,
|
||||
playbook_total=playbook_stats.get("total", 0),
|
||||
incidents_24h=db_metrics.get("incidents_24h", 0),
|
||||
auto_repair_success_24h=db_metrics.get("auto_repair_success_24h", 0),
|
||||
)
|
||||
|
||||
return snapshot
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Internal helpers
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def _count_mcp_calls_24h(since_24h) -> int:
|
||||
"""
|
||||
MCP 呼叫次數/24h。
|
||||
|
||||
Phase 0:無 MCP Calls Table → 從 audit_logs 嘗試計數。
|
||||
Phase 1 建立 PreDecisionInvestigator 後,此處改為查 mcp_tool_calls 表。
|
||||
"""
|
||||
try:
|
||||
async with get_db_context() as db:
|
||||
# audit_logs 中 action='mcp_call' — Phase 0 預期 0 筆
|
||||
result = await db.execute(
|
||||
text(
|
||||
"SELECT COUNT(*) FROM audit_logs "
|
||||
"WHERE action = 'mcp_call' AND created_at >= :since"
|
||||
),
|
||||
{"since": since_24h},
|
||||
)
|
||||
return result.scalar_one_or_none() or 0
|
||||
except Exception:
|
||||
logger.exception("baseline_mcp_count_error")
|
||||
return 0
|
||||
|
||||
|
||||
async def _playbook_confidence_stats() -> dict:
|
||||
"""
|
||||
掃描 Redis 中全部 Playbook,統計 ai_confidence 分佈。
|
||||
|
||||
指標診斷:
|
||||
- avg_confidence ≈ 0.3 → 佐證「全靜態」現況(Phase 0 基線)
|
||||
- Phase 3 EWMA 上線後,此值應動態分散(std_dev 升高、avg 可能提升)
|
||||
"""
|
||||
stats = {
|
||||
"total": 0,
|
||||
"approved": 0,
|
||||
"avg_confidence": 0.0,
|
||||
"min_confidence": None,
|
||||
"max_confidence": None,
|
||||
"never_used": 0, # success_count + failure_count == 0
|
||||
"action_type_dist": {},
|
||||
}
|
||||
|
||||
try:
|
||||
redis = get_redis()
|
||||
confidences: list[float] = []
|
||||
action_counts: dict[str, int] = {}
|
||||
|
||||
async for key in redis.scan_iter(match=f"{PLAYBOOK_KEY_PREFIX}PB-*", count=200):
|
||||
raw = await redis.get(key)
|
||||
if not raw:
|
||||
continue
|
||||
try:
|
||||
pb = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
stats["total"] += 1
|
||||
|
||||
if pb.get("status") == "approved":
|
||||
stats["approved"] += 1
|
||||
|
||||
conf = pb.get("ai_confidence", 0.0) or 0.0
|
||||
confidences.append(conf)
|
||||
|
||||
used = (pb.get("success_count", 0) or 0) + (pb.get("failure_count", 0) or 0)
|
||||
if used == 0:
|
||||
stats["never_used"] += 1
|
||||
|
||||
# 統計 repair_steps 中首個 action_type(代表主要修復動作)
|
||||
steps = pb.get("repair_steps", [])
|
||||
if steps:
|
||||
first_action = steps[0].get("action_type", "unknown")
|
||||
action_counts[first_action] = action_counts.get(first_action, 0) + 1
|
||||
|
||||
if confidences:
|
||||
stats["avg_confidence"] = round(sum(confidences) / len(confidences), 4)
|
||||
stats["min_confidence"] = round(min(confidences), 4)
|
||||
stats["max_confidence"] = round(max(confidences), 4)
|
||||
|
||||
# RESTART 比例:佐證 ADR-080 診斷(目標 < 40%)
|
||||
total_actions = sum(action_counts.values())
|
||||
restart_count = action_counts.get("restart_service", 0)
|
||||
stats["restart_ratio"] = round(restart_count / total_actions, 4) if total_actions else 0.0
|
||||
stats["action_type_dist"] = action_counts
|
||||
|
||||
except Exception:
|
||||
logger.exception("baseline_playbook_stats_error")
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
async def _db_metrics(since_24h) -> dict:
|
||||
"""
|
||||
從 PostgreSQL 取得核心計數指標。
|
||||
"""
|
||||
metrics: dict = {
|
||||
"incidents_24h": 0,
|
||||
"incidents_total": 0,
|
||||
"general_alert_ratio": 0.0,
|
||||
"auto_repair_24h": 0,
|
||||
"auto_repair_success_24h": 0,
|
||||
"km_total": 0,
|
||||
"km_vectorized": 0,
|
||||
"learning_writes_24h": 0,
|
||||
"audit_logs_24h": 0,
|
||||
}
|
||||
|
||||
try:
|
||||
async with get_db_context() as db:
|
||||
# Incident 數量(24h + 總計)
|
||||
r = await db.execute(
|
||||
select(func.count(IncidentRecord.incident_id)).where(
|
||||
IncidentRecord.created_at >= since_24h
|
||||
)
|
||||
)
|
||||
metrics["incidents_24h"] = r.scalar_one_or_none() or 0
|
||||
|
||||
r = await db.execute(select(func.count(IncidentRecord.incident_id)))
|
||||
metrics["incidents_total"] = r.scalar_one_or_none() or 0
|
||||
|
||||
# general 告警比例(alert_category = 'general')
|
||||
r = await db.execute(
|
||||
select(func.count()).where(
|
||||
IncidentRecord.alert_category == "general"
|
||||
)
|
||||
)
|
||||
general_count = r.scalar_one_or_none() or 0
|
||||
total = metrics["incidents_total"]
|
||||
metrics["general_alert_ratio"] = round(general_count / total, 4) if total else 0.0
|
||||
|
||||
# 自動修復執行(24h)
|
||||
r = await db.execute(
|
||||
select(func.count(AutoRepairExecution.id)).where(
|
||||
AutoRepairExecution.created_at >= since_24h
|
||||
)
|
||||
)
|
||||
metrics["auto_repair_24h"] = r.scalar_one_or_none() or 0
|
||||
|
||||
r = await db.execute(
|
||||
select(func.count(AutoRepairExecution.id)).where(
|
||||
AutoRepairExecution.created_at >= since_24h,
|
||||
AutoRepairExecution.success.is_(True),
|
||||
)
|
||||
)
|
||||
metrics["auto_repair_success_24h"] = r.scalar_one_or_none() or 0
|
||||
|
||||
# KM 數量 + 向量化率
|
||||
r = await db.execute(select(func.count(KnowledgeEntryRecord.id)))
|
||||
metrics["km_total"] = r.scalar_one_or_none() or 0
|
||||
|
||||
r = await db.execute(
|
||||
select(func.count()).where(
|
||||
KnowledgeEntryRecord.embedding.is_not(None)
|
||||
)
|
||||
)
|
||||
metrics["km_vectorized"] = r.scalar_one_or_none() or 0
|
||||
|
||||
# 學習寫入數(24h 內新增 KM)
|
||||
r = await db.execute(
|
||||
select(func.count()).where(
|
||||
KnowledgeEntryRecord.created_at >= since_24h
|
||||
)
|
||||
)
|
||||
metrics["learning_writes_24h"] = r.scalar_one_or_none() or 0
|
||||
|
||||
# audit_logs 24h 計數(Phase 0 預期 = 0)
|
||||
r = await db.execute(
|
||||
text(
|
||||
"SELECT COUNT(*) FROM audit_logs WHERE created_at >= :since"
|
||||
),
|
||||
{"since": since_24h},
|
||||
)
|
||||
metrics["audit_logs_24h"] = r.scalar_one_or_none() or 0
|
||||
|
||||
except Exception:
|
||||
logger.exception("baseline_db_metrics_error")
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
def _compute_learning_rate(auto_repair_24h: int, learning_writes_24h: int) -> float:
|
||||
"""
|
||||
學習閉環觸發率 = learning_writes_24h / auto_repair_24h。
|
||||
|
||||
Phase 0 診斷:fire-and-forget → 比率為 0%(即使 auto_repair > 0,learning 也可能 = 0)
|
||||
Phase 3 修復後目標:≥ 99%
|
||||
"""
|
||||
if auto_repair_24h == 0:
|
||||
return 0.0
|
||||
return round(min(learning_writes_24h / auto_repair_24h, 1.0), 4)
|
||||
|
||||
|
||||
async def _persist_to_redis(ts_iso: str, snapshot: dict) -> None:
|
||||
"""
|
||||
將快照寫入 Redis:
|
||||
- `aiops:baseline:{ts_iso}` — 歷史記錄(永不過期)
|
||||
- `aiops:baseline:latest` — 最新快照全量(永不過期)
|
||||
"""
|
||||
try:
|
||||
redis = get_redis()
|
||||
payload = json.dumps(snapshot, ensure_ascii=False)
|
||||
|
||||
# 歷史記錄(保留全部 snapshot)
|
||||
await redis.set(f"{BASELINE_KEY_PREFIX}{ts_iso}", payload)
|
||||
|
||||
# 最新快照(供 API 快速讀取)
|
||||
await redis.set(BASELINE_LATEST_KEY, payload)
|
||||
|
||||
logger.info("baseline_snapshot_persisted", key=BASELINE_LATEST_KEY)
|
||||
except Exception:
|
||||
logger.exception("baseline_persist_error")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Entry point(直接執行)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async def _main() -> None:
|
||||
snapshot = await take_baseline_snapshot()
|
||||
print(json.dumps(snapshot, indent=2, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(_main())
|
||||
@@ -69,6 +69,7 @@ from src.api.v1 import terminal as terminal_v1 # Phase 19.1: Omni-Terminal SSE
|
||||
from src.api.v1 import timeline as timeline_v1
|
||||
from src.api.v1 import webhooks as webhooks_v1
|
||||
from src.core.config import settings
|
||||
from src.core.feature_flags import aiops_flags # ADR-080: AI 自主化飛輪 feature flags 啟動驗證
|
||||
from src.core.http_client import close_all_http_clients, init_all_http_clients
|
||||
from src.core.logging import get_logger, setup_logging
|
||||
from src.core.redis_client import close_redis_pool, init_redis_pool
|
||||
|
||||
Reference in New Issue
Block a user