fix(governance): intake km healthcheck dispatches
This commit is contained in:
@@ -29,8 +29,13 @@ from src.db.models import (
|
||||
IncidentEvidence,
|
||||
KnowledgeEntryRecord,
|
||||
PlaybookRecord,
|
||||
generate_uuid,
|
||||
)
|
||||
from src.models.knowledge import EntryStatus
|
||||
from src.repositories.governance_remediation_dispatch_repo import (
|
||||
DispatchAlreadyActive,
|
||||
create_dispatch,
|
||||
)
|
||||
from src.utils.timezone import now_taipei
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -713,19 +718,26 @@ class GovernanceAgent:
|
||||
ADR-085 鐵律:AI 學習成果不可存 Cache,必須落地 PG
|
||||
"""
|
||||
# 1. 寫 PG(ADR-085 鐵律 — 失敗不阻斷主流程)
|
||||
event_id = generate_uuid()
|
||||
pg_written = False
|
||||
try:
|
||||
from sqlalchemy import insert as _sa_insert
|
||||
async with get_db_context() as db:
|
||||
await db.execute(
|
||||
_sa_insert(AiGovernanceEvent).values(
|
||||
id=event_id,
|
||||
event_type=event_type,
|
||||
details=payload,
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
pg_written = True
|
||||
except Exception as _pg_err:
|
||||
logger.warning("governance_pg_write_failed", error=str(_pg_err))
|
||||
|
||||
if pg_written:
|
||||
await _maybe_create_intake_dispatch(event_id, event_type, payload)
|
||||
|
||||
# 2. structlog(保留既有行為)
|
||||
logger.warning("governance_alert", event_type=event_type, **payload)
|
||||
|
||||
@@ -745,6 +757,102 @@ class GovernanceAgent:
|
||||
logger.warning("governance_telegram_alert_failed", error=str(e))
|
||||
|
||||
|
||||
async def _maybe_create_intake_dispatch(
|
||||
event_id: str,
|
||||
event_type: str,
|
||||
payload: dict[str, Any],
|
||||
) -> None:
|
||||
"""把可行動治理告警同步轉成 non-executing dispatch work item。
|
||||
|
||||
這層只建立可追蹤派工,不執行修復、不寫 KM、不發額外通知。
|
||||
後續 Hermes KB growth worker / GovernanceDispatcher 可以接續推進狀態。
|
||||
"""
|
||||
if event_type != "knowledge_degradation":
|
||||
return
|
||||
|
||||
try:
|
||||
await create_dispatch(
|
||||
event_id=event_id,
|
||||
event_type=event_type,
|
||||
executor_type="hermes_kb_growth_healthcheck",
|
||||
decision_context=_build_knowledge_degradation_dispatch_context(event_id, payload),
|
||||
max_attempts=1,
|
||||
created_by="governance_agent_intake",
|
||||
)
|
||||
except DispatchAlreadyActive:
|
||||
logger.info(
|
||||
"governance_intake_dispatch_already_active",
|
||||
event_id=event_id,
|
||||
event_type=event_type,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"governance_intake_dispatch_failed",
|
||||
event_id=event_id,
|
||||
event_type=event_type,
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
|
||||
def _build_knowledge_degradation_dispatch_context(
|
||||
event_id: str,
|
||||
payload: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
impact = payload.get("impact") if isinstance(payload.get("impact"), dict) else {}
|
||||
remediation = payload.get("remediation") if isinstance(payload.get("remediation"), dict) else {}
|
||||
ownership = payload.get("ownership") if isinstance(payload.get("ownership"), dict) else {}
|
||||
next_action = remediation.get("next_action")
|
||||
if not isinstance(next_action, str) or not next_action:
|
||||
next_action = "run_kb_growth_healthcheck"
|
||||
|
||||
return {
|
||||
"version": "v1",
|
||||
"trigger_source": "governance_agent",
|
||||
"triggered_metric": "knowledge_degradation",
|
||||
"metric_value": impact.get("stale_ratio"),
|
||||
"threshold": impact.get("threshold"),
|
||||
"suggested_action": next_action,
|
||||
"next_action": next_action,
|
||||
"decision_path": "pending_owner_review",
|
||||
"ownership": ownership,
|
||||
"affected_resources": ["knowledge_entries"],
|
||||
"workflow": {
|
||||
"work_item_id": f"governance:knowledge_degradation:{event_id}",
|
||||
"work_kind": "kb_growth_healthcheck",
|
||||
"current_stage": "queued_kb_healthcheck",
|
||||
"steps": [
|
||||
"detected",
|
||||
"ai_analyzed",
|
||||
"queued_kb_healthcheck",
|
||||
"draft_km_updates",
|
||||
"waiting_owner_review",
|
||||
"km_writeback_after_approval",
|
||||
"stale_ratio_recheck",
|
||||
],
|
||||
"stage_by_dispatch_status": {
|
||||
"pending": "queued_kb_healthcheck",
|
||||
"dispatched": "queued_kb_healthcheck",
|
||||
"executing": "draft_km_updates",
|
||||
"succeeded": "stale_ratio_recheck",
|
||||
"failed": "needs_manual_km_triage",
|
||||
"skipped": "waiting_owner_review",
|
||||
"cancelled": "cancelled",
|
||||
},
|
||||
"next_action": next_action,
|
||||
"needs_human_review": True,
|
||||
"writes_km_without_approval": False,
|
||||
"impact": impact,
|
||||
},
|
||||
"extra": {
|
||||
"event_id": event_id,
|
||||
"stale_count": impact.get("stale_count"),
|
||||
"total_count": impact.get("total_count"),
|
||||
"stale_days": impact.get("stale_days"),
|
||||
"ownership": ownership,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Singleton + 排程迴圈
|
||||
# =============================================================================
|
||||
|
||||
@@ -157,6 +157,11 @@ async def dispatch_governance_event(event: AiGovernanceEvent) -> str | None:
|
||||
)
|
||||
return None
|
||||
|
||||
# knowledge_degradation 的 run_kb_growth_healthcheck 是治理工作項 intake,
|
||||
# 不是自動修復執行;先落 pending dispatch,讓既有 unresolved 事件也能在 AwoooP 追蹤。
|
||||
if _is_kb_growth_healthcheck_event(event):
|
||||
return await _record_kb_growth_healthcheck_dispatch(event)
|
||||
|
||||
# Step 2: 決策融合(三維:LLM × Playbook × MCP)
|
||||
adapter = get_decision_fusion_adapter()
|
||||
try:
|
||||
@@ -259,6 +264,49 @@ async def dispatch_governance_event(event: AiGovernanceEvent) -> str | None:
|
||||
return dispatch_row.id
|
||||
|
||||
|
||||
async def _record_kb_growth_healthcheck_dispatch(event: AiGovernanceEvent) -> str | None:
|
||||
"""將 knowledge_degradation intake 成 Hermes KB healthcheck work item."""
|
||||
try:
|
||||
from src.services.governance_agent import (
|
||||
_build_knowledge_degradation_dispatch_context,
|
||||
)
|
||||
|
||||
dispatch_row = await create_dispatch(
|
||||
event_id=event.id,
|
||||
event_type=event.event_type,
|
||||
executor_type="hermes_kb_growth_healthcheck",
|
||||
decision_context=_build_knowledge_degradation_dispatch_context(
|
||||
event.id,
|
||||
event.details if isinstance(event.details, dict) else {},
|
||||
),
|
||||
max_attempts=1,
|
||||
created_by="governance_dispatcher_intake",
|
||||
)
|
||||
except DispatchAlreadyActive:
|
||||
logger.info(
|
||||
"governance_kb_healthcheck_dispatch_race_condition",
|
||||
event_id=event.id,
|
||||
event_type=event.event_type,
|
||||
)
|
||||
return None
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"governance_kb_healthcheck_dispatch_failed",
|
||||
event_id=event.id,
|
||||
event_type=event.event_type,
|
||||
error=str(exc),
|
||||
)
|
||||
return None
|
||||
|
||||
logger.info(
|
||||
"governance_kb_healthcheck_dispatched",
|
||||
dispatch_id=dispatch_row.id,
|
||||
event_id=event.id,
|
||||
event_type=event.event_type,
|
||||
)
|
||||
return dispatch_row.id
|
||||
|
||||
|
||||
async def _record_skipped_dispatch(event: AiGovernanceEvent, decision: FusedDecision) -> None:
|
||||
"""留下 skipped 派遣 trail,讓治理事件的 AI 判斷不再只存在 log 裡。
|
||||
|
||||
@@ -365,6 +413,16 @@ def _build_decision_context(
|
||||
}
|
||||
|
||||
|
||||
def _is_kb_growth_healthcheck_event(event: AiGovernanceEvent) -> bool:
|
||||
if event.event_type != "knowledge_degradation":
|
||||
return False
|
||||
details = event.details if isinstance(event.details, dict) else {}
|
||||
remediation = details.get("remediation")
|
||||
if isinstance(remediation, dict) and remediation.get("next_action") == "run_kb_growth_healthcheck":
|
||||
return True
|
||||
return details.get("next_action") == "run_kb_growth_healthcheck"
|
||||
|
||||
|
||||
def _extract_event_next_action(details: dict[str, Any], fallback: str) -> str:
|
||||
remediation = details.get("remediation")
|
||||
if isinstance(remediation, dict):
|
||||
|
||||
Reference in New Issue
Block a user