feat(nemoton): 新增 route_to_km + mark_for_relearn 工具
All checks were successful
CD Pipeline / deploy (push) Successful in 1m7s

- route_to_km: NIM 決策後靜默歸檔洞察到指定 KM 領域
  (price_competition / sales_anomaly / promotion_opportunity / market_trend)
- mark_for_relearn: 新數據推翻歷史洞察時,批次更新 ai_insights.status='relearn'
  + feedback_down+1,供品質分數重算批次感知
- TOOL_MAP 加入兩個新 handler,Python 獨裁層補 route_to_km threat 注入

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ogt
2026-04-19 11:26:48 +08:00
parent 709efb6e37
commit 8c6fe961cb

View File

@@ -203,6 +203,57 @@ TOOLS = [
},
},
},
{
"type": "function",
"function": {
"name": "route_to_km",
"description": (
"將商品競價洞察路由到知識庫KM的指定領域分類"
"供未來 RAG 查詢與 OpenClaw 週報引用。"
"適用於:數據有參考價值但不需立即告警的情況。"
),
"parameters": {
"type": "object",
"properties": {
"sku": {"type": "string", "description": "商品 SKU 編號"},
"name": {"type": "string", "description": "商品名稱"},
"km_domain": {
"type": "string",
"description": (
"KM 領域分類,必須為以下之一:"
"price_competition競價情報"
"sales_anomaly銷量異常"
"promotion_opportunity促銷機會"
"market_trend市場趨勢"
),
},
"summary": {"type": "string", "description": "此洞察的核心摘要繁體中文50 字內)"},
"confidence": {"type": "number", "description": "AI 信心度 0.0~1.0"},
},
"required": ["sku", "name", "km_domain", "summary", "confidence"],
},
},
},
{
"type": "function",
"function": {
"name": "mark_for_relearn",
"description": (
"當新數據與 KM 既有洞察矛盾,或告警方向被推翻時,"
"將該商品的歷史洞察標記為需重新學習relearn"
"適用於:此次分析結果與上次截然不同的情況。"
),
"parameters": {
"type": "object",
"properties": {
"sku": {"type": "string", "description": "商品 SKU 編號"},
"name": {"type": "string", "description": "商品名稱"},
"reason": {"type": "string", "description": "標記原因說明(繁體中文)"},
},
"required": ["sku", "name", "reason"],
},
},
},
]
@@ -737,6 +788,73 @@ class NemotronDispatcher:
"momo_price": momo_price, "comp_price": comp_price},
)
def _exec_route_to_km(
self,
sku: str, name: str, km_domain: str, summary: str, confidence: float,
footprint: str = "",
threat=None,
):
"""
將洞察路由到 KM 指定領域sink 到 ai_insights 供 RAG 使用。
不送 Telegram 告警(靜默操作,僅 log
"""
_KM_DOMAINS = {"price_competition", "sales_anomaly", "promotion_opportunity", "market_trend"}
domain = km_domain if km_domain in _KM_DOMAINS else "price_competition"
summary = _sanitize_text(summary, fallback="競價洞察已歸檔")
self._sink_insight_to_km(
insight_type=f"km_{domain}",
sku=sku, name=name,
content=f"[KM 路由 {domain}] {name}{summary}",
metadata={
"km_domain": domain,
"confidence": confidence,
"momo_price": getattr(threat, "momo_price", None) if threat else None,
"pchome_price": getattr(threat, "pchome_price", None) if threat else None,
"gap_pct": getattr(threat, "gap_pct", None) if threat else None,
"sales_delta": getattr(threat, "sales_7d_delta_pct", None) if threat else None,
},
)
logger.info(f"[Dispatcher] KM 路由 → {sku} domain={domain} confidence={confidence:.2f}")
def _exec_mark_for_relearn(
self,
sku: str, name: str, reason: str,
footprint: str = "",
):
"""
將該 SKU 的既有 ai_insights 標記 status='relearn' + feedback_down+1
讓每日去重/品質分數重算批次可感知「此洞察已被推翻」。
不送 Telegram 告警(靜默操作,僅 log
"""
reason = _sanitize_text(reason, fallback="新數據與歷史洞察矛盾,需重新學習")
try:
from database.manager import DatabaseManager
db = DatabaseManager()
with db.get_session() as session:
from sqlalchemy import text
result = session.execute(text("""
UPDATE ai_insights
SET status = 'relearn',
feedback_down = COALESCE(feedback_down, 0) + 1,
updated_at = CURRENT_TIMESTAMP
WHERE product_sku = :sku
AND status NOT IN ('relearn', 'archived')
"""), {"sku": sku})
session.commit()
rows = result.rowcount
logger.info(f"[Dispatcher] mark_for_relearn → {sku} 共更新 {rows} 筆洞察;原因:{reason}")
except Exception as e:
logger.warning(f"[Dispatcher] mark_for_relearn DB 更新失敗 ({sku}): {e}")
# 同時寫入一筆 relearn 事件到 ai_insights 留存紀錄
self._sink_insight_to_km(
insight_type="relearn_event",
sku=sku, name=name,
content=f"[重新學習事件] {name}{reason}",
metadata={"sku": sku, "trigger": "nemoton_dispatcher"},
)
def _sink_insight_to_km(self, insight_type: str, sku: str, name: str,
content: str, metadata: dict = None):
"""
@@ -948,6 +1066,8 @@ class NemotronDispatcher:
"trigger_price_alert": self._exec_trigger_price_alert,
"add_to_recommendation": self._exec_add_to_recommendation,
"flag_for_human_review": self._exec_flag_for_human_review,
"route_to_km": self._exec_route_to_km,
"mark_for_relearn": self._exec_mark_for_relearn,
}
for tc in tool_calls:
@@ -979,6 +1099,9 @@ class NemotronDispatcher:
elif tool_name == "add_to_recommendation":
args["footprint_data"] = footprint_data
args["threat"] = t
elif tool_name == "route_to_km":
args["threat"] = t
# mark_for_relearn 無需注入客觀數字(僅寫 DB
try:
handler(**args)