This commit is contained in:
@@ -6,7 +6,7 @@ L2 NemoTron 可安全呼叫的動作集合。嚴格限制:
|
||||
- 不可動 prod 資料表 / 容器 / 外部系統
|
||||
- 所有 action 必須 dual-write 審計軌跡
|
||||
|
||||
現階段為 **stub + 完整 interface**,供 event_router 串接。真實執行邏輯將於 Phase 3 填入。
|
||||
EventRouter 只會執行本檔 SAFE_ACTIONS;所有動作必須保守、可審計、可回放。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -51,6 +51,29 @@ def _audit(action: str, params: dict, result: dict, latency_ms: float) -> int |
|
||||
return None
|
||||
|
||||
|
||||
def _store_action_memory(
|
||||
insight_type: str,
|
||||
content: str,
|
||||
*,
|
||||
product_sku: str | None = None,
|
||||
metadata: dict | None = None,
|
||||
status: str = "approved",
|
||||
) -> int | None:
|
||||
"""Write a concrete L2 action outcome into OpenClaw memory."""
|
||||
from services.openclaw_learning_service import store_insight
|
||||
return store_insight(
|
||||
insight_type=insight_type,
|
||||
content=content,
|
||||
period=datetime.now().strftime("%Y-%m-%d"),
|
||||
product_sku=product_sku,
|
||||
metadata=metadata or {},
|
||||
ai_model="agent_actions",
|
||||
confidence=0.8,
|
||||
created_by="agent_actions",
|
||||
status=status,
|
||||
)
|
||||
|
||||
|
||||
ALLOWED_RETRY_TASKS = {
|
||||
"run_auto_import_task", "run_momo_task", "run_edm_task",
|
||||
"run_competitor_price_feeder_task", "run_backup_monitor_task",
|
||||
@@ -205,28 +228,46 @@ def is_silenced(event_key: str) -> bool:
|
||||
# 🏷️ 三個既有 NemoTron tool 的 wrapper(供 event_router 統一調用)
|
||||
# =====================================================================
|
||||
def flag_for_human_review(sku: str, concern: str) -> dict:
|
||||
"""升級到 L3 HITL(包裝 NemoTron 既有 tool,保持呼叫介面一致)"""
|
||||
"""升級到 L3 HITL:寫入 human_review 記憶,等待人工後續處理。"""
|
||||
t0 = time.time()
|
||||
# TODO Phase 3: 接入 nemoton_dispatcher_service._exec_flag_for_human_review
|
||||
result = {"status": "stub", "sku": sku, "concern": concern,
|
||||
"note": "Phase 1 stub,Phase 3 接 NemoTron"}
|
||||
insight_id = _store_action_memory(
|
||||
"human_review",
|
||||
f"SKU={sku} 需要人工覆核:{concern}",
|
||||
product_sku=sku,
|
||||
metadata={"sku": sku, "concern": concern, "source": "flag_for_human_review"},
|
||||
status="pending",
|
||||
)
|
||||
result = {"status": "pending_review", "sku": sku, "concern": concern, "insight_id": insight_id}
|
||||
_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)"""
|
||||
"""KM 歸檔:將 NemoTron/Hermes 判斷沉澱為可檢索知識。"""
|
||||
t0 = time.time()
|
||||
result = {"status": "stub", "note": "Phase 3 接 NemoTron"}
|
||||
insight_id = _store_action_memory(
|
||||
"km_entry",
|
||||
f"[{domain}] SKU={sku}:{summary}",
|
||||
product_sku=sku,
|
||||
metadata={"sku": sku, "domain": domain, "summary": summary, "source": "route_to_km"},
|
||||
)
|
||||
result = {"status": "archived", "sku": sku, "domain": domain, "insight_id": insight_id}
|
||||
_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)"""
|
||||
"""標記重新訓練:寫入 relearn_marker 供 OpenClaw/品質批次使用。"""
|
||||
t0 = time.time()
|
||||
result = {"status": "stub", "note": "Phase 3 接 NemoTron"}
|
||||
insight_id = _store_action_memory(
|
||||
"relearn_marker",
|
||||
f"SKU={sku} 需要重新學習:{reason}",
|
||||
product_sku=sku,
|
||||
metadata={"sku": sku, "reason": reason, "source": "mark_for_relearn"},
|
||||
status="pending",
|
||||
)
|
||||
result = {"status": "marked", "sku": sku, "reason": reason, "insight_id": insight_id}
|
||||
_audit("mark_for_relearn", {"sku": sku, "reason": reason}, result, (time.time() - t0) * 1000)
|
||||
return result
|
||||
|
||||
|
||||
50
tests/test_agent_actions.py
Normal file
50
tests/test_agent_actions.py
Normal file
@@ -0,0 +1,50 @@
|
||||
def test_flag_for_human_review_writes_pending_memory(monkeypatch):
|
||||
import services.agent_actions as actions
|
||||
import services.openclaw_learning_service as learning
|
||||
|
||||
calls = []
|
||||
monkeypatch.setattr(actions, "_audit", lambda *args, **kwargs: 999)
|
||||
monkeypatch.setattr(
|
||||
learning,
|
||||
"store_insight",
|
||||
lambda **kwargs: calls.append(kwargs) or 123,
|
||||
)
|
||||
|
||||
result = actions.flag_for_human_review("SKU-1", "銷量斷崖,請人工確認")
|
||||
|
||||
assert result["status"] == "pending_review"
|
||||
assert result["insight_id"] == 123
|
||||
assert calls[0]["insight_type"] == "human_review"
|
||||
assert calls[0]["status"] == "pending"
|
||||
assert calls[0]["product_sku"] == "SKU-1"
|
||||
|
||||
|
||||
def test_route_to_km_writes_archived_memory(monkeypatch):
|
||||
import services.agent_actions as actions
|
||||
import services.openclaw_learning_service as learning
|
||||
|
||||
calls = []
|
||||
monkeypatch.setattr(actions, "_audit", lambda *args, **kwargs: 999)
|
||||
monkeypatch.setattr(learning, "store_insight", lambda **kwargs: calls.append(kwargs) or 456)
|
||||
|
||||
result = actions.route_to_km("SKU-2", "pricing", "競品價差擴大")
|
||||
|
||||
assert result == {"status": "archived", "sku": "SKU-2", "domain": "pricing", "insight_id": 456}
|
||||
assert calls[0]["insight_type"] == "km_entry"
|
||||
assert calls[0]["metadata"]["domain"] == "pricing"
|
||||
|
||||
|
||||
def test_mark_for_relearn_writes_pending_marker(monkeypatch):
|
||||
import services.agent_actions as actions
|
||||
import services.openclaw_learning_service as learning
|
||||
|
||||
calls = []
|
||||
monkeypatch.setattr(actions, "_audit", lambda *args, **kwargs: 999)
|
||||
monkeypatch.setattr(learning, "store_insight", lambda **kwargs: calls.append(kwargs) or 789)
|
||||
|
||||
result = actions.mark_for_relearn("SKU-3", "NemoTron 信心不足")
|
||||
|
||||
assert result["status"] == "marked"
|
||||
assert result["insight_id"] == 789
|
||||
assert calls[0]["insight_type"] == "relearn_marker"
|
||||
assert calls[0]["status"] == "pending"
|
||||
Reference in New Issue
Block a user