feat(flywheel): W1 PR-P1 + ADR-091 T1 — 飛輪 80→90 第一波
依 onboarder 端到端閉環審計挖出的 10 條斷鏈 + critic 鐵律違反全景, W1 第一波修復飛輪鐵證 1 + 2 的核心斷鏈 C1。 ## W1 PR-P1 — matched_playbook_id 四斷點守門 (C1 修復) fullstack 探勘發現 4 斷點之前 session 已修,本 PR 補: - ENABLE_PLAYBOOK_MATCHING feature flag (default=true) rollback: kubectl set env deployment/awoooi-api ENABLE_PLAYBOOK_MATCHING=false - proposal_service._try_playbook_match_id 入口加 flag check - 7 個 e2e 測試補上保護網(之前無測試覆蓋) 斷鏈 C1 證據鏈:proposal_service.generate_proposal() → matched_playbook_id → approval_db → approval_repository → learning_service._update_playbook_stats 24h 後 playbooks.trust_score 應有真實 EWMA 更新。 ## ADR-091 T1 — auto_generate_rule 雙寫 DB (鐵證 1 第一步) 飛輪鐵證 1:alert_rule_catalog.source='ai_generated' 全 codebase 0 筆。 auto_generate_rule() 寫 alert_rules.yaml 但不寫 DB → AI 自學成果與 catalog 雙軌脫鉤。 修法(依 ADR-091 §1 D1): - 新增 _insert_catalog_ai_generated():YAML 寫入成功後雙寫 source='ai_generated', confidence=0.5, review_status='draft', created_by_agent - 新增 _parse_for_to_seconds() helper("30s"/"5m"/"2h" → seconds) - ON CONFLICT (rule_name) DO NOTHING 冪等保證 - transaction 策略:YAML + DB 不在同一 transaction(YAML 已成 SoT,DB 失敗只 log) - ENABLE_AI_RULE_CATALOG_WRITE feature flag (default=true) rollback: kubectl set env deployment/awoooi-api ENABLE_AI_RULE_CATALOG_WRITE=false 13 個測試覆蓋:parse helper 8 + 業務邏輯 5(success/db_fail/idempotent/flag/SQL_lit) ## 驗證 1572 unit tests 全綠(+20 新增:PR-P1 7 + ADR-091 T1 13) ## 期望影響 飛輪自主化評分:42 → 65(+23 = C1 +3 + 鐵證 1 +20) ## 已知債(critic PR review 揭示,下一個 commit 處理) - KMWriter 統一契約 3 條 caller 路徑被旁路(C1/M1/M2) - KMWriter 冪等聲明與實作不符(M3 缺 ON CONFLICT) - Alertmanager equal:[] 爆炸抑制 + 版本未驗(M4/M5) - drift checker regex 脆弱(M7 應改 AST) - governance health score skipped 失真(M6) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,16 @@ class Settings(BaseSettings):
|
||||
description="Phase 24: True=新 AIRouter 路由, False=舊 openclaw.py fallback chain",
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# W1 PR-P1: Playbook 匹配 Feature Flag (2026-04-28 ogt + Claude Sonnet 4.6)
|
||||
# 修復飛輪斷鏈 C1 — proposal_service 填 matched_playbook_id → EWMA 更新
|
||||
# 回滾指令: kubectl set env deployment/awoooi-api ENABLE_PLAYBOOK_MATCHING=false
|
||||
# ==========================================================================
|
||||
ENABLE_PLAYBOOK_MATCHING: bool = Field(
|
||||
default=True,
|
||||
description="W1 PR-P1: True=generate_proposal 時執行 Playbook RAG 匹配並填 matched_playbook_id, False=行為與修復前完全相同(回滾用)",
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# P1-1: KMWriter 統一契約 (2026-04-28 ogt + Claude Sonnet 4.6)
|
||||
# KM_WRITE_AWAIT=true → 強制 await asyncio.wait_for(timeout=KM_WRITE_TIMEOUT_SECONDS)
|
||||
@@ -530,6 +540,16 @@ class Settings(BaseSettings):
|
||||
default=False,
|
||||
description="ADR-095: 啟用 12-Agent ConsensusEngine weights(預設關閉)",
|
||||
)
|
||||
# ==========================================================================
|
||||
# ADR-091 Task T1: AI 自學規則雙寫 alert_rule_catalog (2026-04-28 ogt + Claude Sonnet 4.6)
|
||||
# True=auto_generate_rule() 成功後同步寫入 DB source='ai_generated'
|
||||
# False=回滾開關,只寫 YAML,不寫 DB
|
||||
# 回滾指令: kubectl set env deployment/awoooi-api ENABLE_AI_RULE_CATALOG_WRITE=false
|
||||
# ==========================================================================
|
||||
ENABLE_AI_RULE_CATALOG_WRITE: bool = Field(
|
||||
default=True,
|
||||
description="ADR-091 T1: True=AI 自學規則雙寫 alert_rule_catalog DB, False=僅 YAML(回滾用)",
|
||||
)
|
||||
# 2026-04-27 P3.1-T2-PathA by Claude — DiagAggregator 信號分類層補 PDI
|
||||
# 路徑 A 已啟用:DA 只取 PDI 已收集的 raw 資料做業務邏輯分類(OOMKilled/CrashLoop 等),
|
||||
# 不重複呼叫 K8s/SignOz API(純邏輯分類,不打外部服務)。
|
||||
|
||||
@@ -581,6 +581,118 @@ def _append_rule_to_yaml(rule_yaml: str, alertname: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _parse_for_to_seconds(for_str: str) -> int | None:
|
||||
"""將 Prometheus 'for' 字串(如 '5m', '30s', '1h')轉換為整數秒數。
|
||||
無法解析時回傳 None。
|
||||
"""
|
||||
if not for_str:
|
||||
return None
|
||||
for_str = for_str.strip()
|
||||
mapping = {"s": 1, "m": 60, "h": 3600, "d": 86400}
|
||||
m = re.fullmatch(r"(\d+)([smhd]?)", for_str)
|
||||
if not m:
|
||||
return None
|
||||
value = int(m.group(1))
|
||||
unit = m.group(2) or "s"
|
||||
return value * mapping.get(unit, 1)
|
||||
|
||||
|
||||
async def _insert_catalog_ai_generated(
|
||||
rule_dict: dict,
|
||||
llm_source: str,
|
||||
rule_id: str,
|
||||
alertname_safe: str,
|
||||
) -> None:
|
||||
"""寫入 alert_rule_catalog source='ai_generated'
|
||||
|
||||
冪等:rule_name 唯一索引(alert_rule_catalog_rule_name_key)已存在
|
||||
使用 INSERT ON CONFLICT (rule_name) DO NOTHING
|
||||
|
||||
transaction 策略:YAML + DB 不在同一 transaction
|
||||
YAML 已成功 → DB 失敗只 log warning,不回滾 YAML
|
||||
|
||||
review_status='draft':對應 DB CHECK ('draft','approved','deprecated','retired')
|
||||
confidence=0.50:新規則未驗證,保守初始值
|
||||
|
||||
ADR-091 Task T1 — 2026-04-28 ogt + Claude Sonnet 4.6 (Asia/Taipei)
|
||||
"""
|
||||
from src.core.config import settings as _settings
|
||||
|
||||
if not _settings.ENABLE_AI_RULE_CATALOG_WRITE:
|
||||
logger.debug(
|
||||
"ai_rule_catalog_write_skipped_flag_disabled",
|
||||
alertname=alertname_safe,
|
||||
)
|
||||
return
|
||||
|
||||
from sqlalchemy import text as _sql
|
||||
import src.db.base as _db_base
|
||||
import json as _json
|
||||
|
||||
# 從 rule_dict 提取欄位
|
||||
# 'expr' 在 OpenClaw YAML 規則中不存在(非 PromQL),
|
||||
# 使用 alertname 作為語意佔位(與 yaml_hardcoded 同等策略)
|
||||
expr = rule_dict.get("expr") or f'alertname="{alertname_safe}"'
|
||||
for_str = rule_dict.get("for", "0s")
|
||||
duration_seconds = _parse_for_to_seconds(str(for_str))
|
||||
labels = rule_dict.get("labels", {})
|
||||
annotations = rule_dict.get("annotations", {})
|
||||
|
||||
# 若 LLM 有產出 incident_type,注入 annotations 方便後續 T3 查詢
|
||||
incident_type = rule_dict.get("incident_type")
|
||||
if incident_type and "incident_type" not in annotations:
|
||||
annotations = {**annotations, "incident_type": str(incident_type)}
|
||||
|
||||
# severity 從 labels 取(Prometheus 慣例),兜底空字串
|
||||
severity = (labels.get("severity", "") or "").strip()[:50] or None
|
||||
|
||||
try:
|
||||
async with _db_base.get_db_context() as db:
|
||||
await db.execute(
|
||||
_sql("""
|
||||
INSERT INTO alert_rule_catalog (
|
||||
rule_name, source, expr, duration_seconds,
|
||||
severity, labels, annotations,
|
||||
created_by_agent, confidence, review_status,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
:rule_name, 'ai_generated', :expr, :duration_seconds,
|
||||
:severity,
|
||||
CAST(:labels AS jsonb),
|
||||
CAST(:annotations AS jsonb),
|
||||
:created_by_agent, 0.50, 'draft',
|
||||
NOW(), NOW()
|
||||
)
|
||||
ON CONFLICT (rule_name) DO NOTHING
|
||||
"""),
|
||||
{
|
||||
"rule_name": alertname_safe[:200],
|
||||
"expr": expr[:4000],
|
||||
"duration_seconds": duration_seconds,
|
||||
"severity": severity,
|
||||
"labels": _json.dumps(labels, ensure_ascii=False),
|
||||
"annotations": _json.dumps(annotations, ensure_ascii=False),
|
||||
"created_by_agent": llm_source,
|
||||
},
|
||||
)
|
||||
logger.info(
|
||||
"ai_rule_catalog_insert_success",
|
||||
alertname=alertname_safe,
|
||||
rule_id=rule_id,
|
||||
llm_source=llm_source,
|
||||
outcome="success",
|
||||
)
|
||||
except Exception as db_err:
|
||||
# DB 失敗只 warning,不影響已成功的 YAML 寫入
|
||||
logger.warning(
|
||||
"ai_rule_catalog_insert_failed",
|
||||
alertname=alertname_safe,
|
||||
rule_id=rule_id,
|
||||
error=str(db_err),
|
||||
outcome="failed",
|
||||
)
|
||||
|
||||
|
||||
async def _call_ollama(prompt: str, ollama_url: str, model: str) -> str | None:
|
||||
"""呼叫 Ollama 生成規則 YAML"""
|
||||
try:
|
||||
@@ -749,6 +861,25 @@ async def auto_generate_rule(
|
||||
# 立即為新規則建立 APPROVED Playbook(不等下次重啟)
|
||||
from src.services.playbook_seed_service import seed_playbooks_from_rules
|
||||
_asyncio.create_task(seed_playbooks_from_rules())
|
||||
# ADR-091 T1: 雙寫 alert_rule_catalog source='ai_generated'
|
||||
# 獨立 try/except — DB 失敗不回滾已成功的 YAML 寫入
|
||||
try:
|
||||
parsed_rules = yaml.safe_load(yaml_block)
|
||||
rule_dict = parsed_rules[0] if isinstance(parsed_rules, list) and parsed_rules else {}
|
||||
_asyncio.create_task(
|
||||
_insert_catalog_ai_generated(
|
||||
rule_dict=rule_dict,
|
||||
llm_source=llm_source or "unknown",
|
||||
rule_id=rule_id,
|
||||
alertname_safe=alertname_safe,
|
||||
)
|
||||
)
|
||||
except Exception as _catalog_err:
|
||||
logger.warning(
|
||||
"ai_rule_catalog_task_create_failed",
|
||||
alertname=alertname_safe,
|
||||
error=str(_catalog_err),
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"auto_rule_auto_generate_failed",
|
||||
|
||||
@@ -22,6 +22,7 @@ from datetime import UTC, datetime
|
||||
|
||||
import structlog
|
||||
|
||||
from src.core.config import get_settings
|
||||
from src.db.base import get_db_context
|
||||
from src.db.models import IncidentRecord
|
||||
from src.models.approval import (
|
||||
@@ -373,7 +374,17 @@ class ProposalService:
|
||||
讓學習服務 EWMA 能在人工審核後更新 Playbook trust score。
|
||||
邏輯與 decision_manager._try_playbook_match 相同,但只回傳 ID 不改 action。
|
||||
失敗時靜默返回 None(不阻塞主流程)。
|
||||
|
||||
W1 PR-P1 Feature Flag (2026-04-28 ogt + Claude Sonnet 4.6):
|
||||
ENABLE_PLAYBOOK_MATCHING=false → 回傳 None,行為與修復前完全相同(回滾用)。
|
||||
"""
|
||||
if not get_settings().ENABLE_PLAYBOOK_MATCHING:
|
||||
logger.debug(
|
||||
"playbook_matching_disabled",
|
||||
incident_id=getattr(incident, "incident_id", "?"),
|
||||
)
|
||||
return None
|
||||
|
||||
PLAYBOOK_SIMILARITY_THRESHOLD = 0.85
|
||||
try:
|
||||
from src.models.playbook import SymptomPattern
|
||||
|
||||
Reference in New Issue
Block a user