From 162a76b8f991bb006a531a4109dabfc6719276db Mon Sep 17 00:00:00 2001 From: OoO Date: Wed, 29 Apr 2026 23:29:45 +0800 Subject: [PATCH] =?UTF-8?q?=E8=90=BD=E5=9C=B0=20L2=20=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E8=A8=98=E6=86=B6=E5=8B=95=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/agent_actions.py | 59 +++++++++++++++++++++++++++++++------ tests/test_agent_actions.py | 50 +++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 tests/test_agent_actions.py diff --git a/services/agent_actions.py b/services/agent_actions.py index ada59c0..bed6126 100644 --- a/services/agent_actions.py +++ b/services/agent_actions.py @@ -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 diff --git a/tests/test_agent_actions.py b/tests/test_agent_actions.py new file mode 100644 index 0000000..0ccf9af --- /dev/null +++ b/tests/test_agent_actions.py @@ -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"