Files
ewoooc/services/agent_actions.py
ogt 0b4f80ee8a
All checks were successful
CD Pipeline / deploy (push) Successful in 1m14s
feat(ai-ops): Agent Action Ladder 骨幹(ADR-012 Phase 1)+ 週報套模板
ADR-012 核心設計:
- 4 級信任邊界:L0 直出 / L1 Hermes 觀察 / L2 NemoTron 診斷執行 / L3 OpenClaw HITL
- 通知鏈絕不中斷:每級失敗立即降級,保底 L0 模板 + 🟡 標記
- Audit Trail:每次 dispatch 自動寫 ai_insights (insight_type=agent_action)
- 安全白名單:L2 可呼叫 6 個安全 action(retry/query_km/silence + 3 個既有 NemoTron tool)

新增檔案:
- services/event_router.py — 事件分流入口,按 severity × event_type 分 Tier
- services/agent_actions.py — 安全 action 白名單(Phase 1 stub + 完整介面)
- docs/adr/ADR-012-agent-action-ladder.md — 完整設計 + 分階段計畫

Phase 1 狀態:
- L0 直出完整可用 
- L1 Hermes / L2 NemoTron 為 stub(Phase 2/3 填實作)
- Fallback 降級鏈已完整 
- 靜音檢查(is_silenced)+ Audit Trail 已就緒 

處理既有 TODO:
- services/openclaw_strategist_service.py::_notify_telegram_group()
  改用 telegram_templates.report() 統一週報格式

全景盤點(新 memory):
- reference_telegram_endpoints_map.md — 21 個 Telegram 發送點
- feedback_agent_action_ladder.md — 操作規範
  (+ 既有 ADR-011 跨專案隔離規範一併生效)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 12:46:51 +08:00

176 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Agent Action 白名單ADR-012 Phase 1 骨幹)
L2 NemoTron 可安全呼叫的動作集合。嚴格限制:
- 只能寫 ai_insights 和發 Telegram
- 不可動 prod 資料表 / 容器 / 外部系統
- 所有 action 必須 dual-write 審計軌跡
現階段為 **stub + 完整 interface**,供 event_router 串接。真實執行邏輯將於 Phase 3 填入。
"""
from __future__ import annotations
import time
from datetime import datetime, timedelta
from typing import Any
from services.logger_manager import SystemLogger
sys_log = SystemLogger("AgentAction").get_logger()
# 靜音表記憶體快取重啟後清空Phase 3 可改 DB 持久化)
_silence_table: dict[str, datetime] = {}
def _audit(action: str, params: dict, result: dict, latency_ms: float) -> int | None:
"""所有 action 統一審計入 ai_insightsADR-007 Dual-Write"""
try:
from services.openclaw_learning_service import store_insight
return store_insight(
insight_type="agent_action",
content=f"action={action} result={result.get('status', 'unknown')}",
period=datetime.now().strftime("%Y-%m-%d"),
metadata={
"action": action,
"params": params,
"result": result,
"latency_ms": latency_ms,
"ts": datetime.now().isoformat(),
},
)
except Exception as e:
sys_log.error(f"[AgentAction] audit 失敗 action={action}: {e}")
return None
# =====================================================================
# 🔁 retry_task — 安全重試exponential backoff
# =====================================================================
def retry_task(task_name: str, max_attempts: int = 3, backoff_sec: int = 60) -> dict:
"""
安全重試一個 scheduler task。Phase 1 stub只記錄不真正重試。
Phase 3 將接入 scheduler.py 的 task dispatch。
限制task_name 必須在白名單內(避免任意程式碼執行)
"""
ALLOWED_TASKS = {
"run_auto_import_task", "run_momo_task", "run_edm_task",
"run_competitor_price_feeder_task", "run_backup_monitor_task",
"run_icaim_analysis_task",
}
t0 = time.time()
if task_name not in ALLOWED_TASKS:
result = {"status": "rejected", "reason": f"task '{task_name}' not in whitelist"}
_audit("retry_task", {"task_name": task_name}, result, (time.time() - t0) * 1000)
sys_log.warning(f"[AgentAction] retry_task 拒絕:{task_name} 不在白名單")
return result
# TODO Phase 3: 真實重試邏輯(呼叫 scheduler module 的 task function
result = {
"status": "queued",
"task_name": task_name,
"max_attempts": max_attempts,
"backoff_sec": backoff_sec,
"note": "Phase 1 stub — 尚未真正重試,僅記錄意圖",
}
_audit("retry_task", {"task_name": task_name, "max_attempts": max_attempts},
result, (time.time() - t0) * 1000)
sys_log.info(f"[AgentAction] retry_task 已排隊stub: {task_name}")
return result
# =====================================================================
# 🔍 query_km — RAG 查詢歷史同類事件
# =====================================================================
def query_km(query: str, insight_type: str | None = None, limit: int = 5) -> dict:
"""透過 openclaw_learning_service.build_rag_context 找歷史同類事件"""
t0 = time.time()
try:
from services.openclaw_learning_service import build_rag_context
context = build_rag_context(query=query, insight_type=insight_type)
result = {
"status": "ok",
"query": query,
"context_preview": (context or "")[:500],
"has_results": bool(context and context.strip()),
}
except Exception as e:
result = {"status": "error", "error": str(e)[:200]}
sys_log.error(f"[AgentAction] query_km 失敗: {e}")
_audit("query_km", {"query": query, "insight_type": insight_type, "limit": limit},
result, (time.time() - t0) * 1000)
return result
# =====================================================================
# 🔕 silence_alert — 靜音抑制(避免告警風暴)
# =====================================================================
def silence_alert(event_key: str, duration_min: int = 60) -> dict:
"""
對特定 event_key 設定靜音期限。EventRouter 在 dispatch 前會先檢查。
event_key 建議格式:"<source>:<event_type>",例:
"Scheduler.AutoImport:db_connection_error"
"""
t0 = time.time()
until = datetime.now() + timedelta(minutes=duration_min)
_silence_table[event_key] = until
result = {"status": "silenced", "event_key": event_key, "until": until.isoformat()}
_audit("silence_alert", {"event_key": event_key, "duration_min": duration_min},
result, (time.time() - t0) * 1000)
sys_log.info(f"[AgentAction] silence_alert: {event_key} → 靜音至 {until.strftime('%H:%M')}")
return result
def is_silenced(event_key: str) -> bool:
"""EventRouter 呼叫,判斷是否需略過此事件"""
until = _silence_table.get(event_key)
if until is None:
return False
if datetime.now() >= until:
_silence_table.pop(event_key, None)
return False
return True
# =====================================================================
# 🏷️ 三個既有 NemoTron tool 的 wrapper供 event_router 統一調用)
# =====================================================================
def flag_for_human_review(sku: str, concern: str) -> dict:
"""升級到 L3 HITL包裝 NemoTron 既有 tool保持呼叫介面一致"""
t0 = time.time()
# TODO Phase 3: 接入 nemoton_dispatcher_service._exec_flag_for_human_review
result = {"status": "stub", "sku": sku, "concern": concern,
"note": "Phase 1 stubPhase 3 接 NemoTron"}
_audit("flag_for_human_review", {"sku": sku, "concern": concern},
result, (time.time() - t0) * 1000)
return result
def route_to_km(sku: str, domain: str, summary: str) -> dict:
"""KM 歸檔Phase 3 接 NemoTron"""
t0 = time.time()
result = {"status": "stub", "note": "Phase 3 接 NemoTron"}
_audit("route_to_km", {"sku": sku, "domain": domain}, result, (time.time() - t0) * 1000)
return result
def mark_for_relearn(sku: str, reason: str) -> dict:
"""標記重新訓練Phase 3 接 NemoTron"""
t0 = time.time()
result = {"status": "stub", "note": "Phase 3 接 NemoTron"}
_audit("mark_for_relearn", {"sku": sku, "reason": reason}, result, (time.time() - t0) * 1000)
return result
# 白名單(供 EventRouter / NemoTron 引用)
SAFE_ACTIONS: dict[str, Any] = {
"retry_task": retry_task,
"query_km": query_km,
"silence_alert": silence_alert,
"flag_for_human_review": flag_for_human_review,
"route_to_km": route_to_km,
"mark_for_relearn": mark_for_relearn,
}