feat(nemoton): 新增 route_to_km + mark_for_relearn 工具
All checks were successful
CD Pipeline / deploy (push) Successful in 1m7s
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user