This commit is contained in:
@@ -45,7 +45,7 @@ _HEALTH_INDICATOR_CACHE = {
|
||||
'payload': None,
|
||||
}
|
||||
_HEALTH_INDICATOR_CACHE_TTL_SECONDS = 30
|
||||
_PPT_PUBLIC_RUNTIME_ERROR = '視覺審核暫時無法完成;請先用線上預覽人工確認版面,稍後重新執行審核。'
|
||||
_PPT_PUBLIC_RUNTIME_ERROR = '視覺審核暫時無法完成;請先用線上預覽並由 AI 自動驗證確認版面,稍後重新執行審核。'
|
||||
_PPT_INTERNAL_ERROR_MARKERS = (
|
||||
'all 3 hosts failed',
|
||||
'httpconnectionpool',
|
||||
@@ -144,6 +144,12 @@ def _public_ppt_source_label(source):
|
||||
|
||||
def _public_ppt_vision_status(status):
|
||||
status = dict(status or {})
|
||||
raw_blockers = [str(item).strip() for item in status.get('blockers') or [] if str(item).strip()]
|
||||
status['runtime_dependency_blockers'] = [
|
||||
item
|
||||
for item in raw_blockers
|
||||
if 'PPT_VISION_ENABLED' in item or 'LibreOffice' in item
|
||||
]
|
||||
status['summary'] = _public_ppt_text(
|
||||
status.get('summary'),
|
||||
empty='視覺檢查狀態待確認。',
|
||||
@@ -3133,11 +3139,11 @@ def _enrich_ppt_coverage_items(auto_generation_items, files, generation_runs, au
|
||||
run_status = latest_run.get('status') or ''
|
||||
|
||||
if latest_run and run_status == 'error':
|
||||
db_status, db_label = 'error', '紀錄失敗'
|
||||
db_status, db_label = 'error', 'DB 寫入失敗'
|
||||
elif db_backed:
|
||||
db_status, db_label = 'ready', '已記錄'
|
||||
db_status, db_label = 'ready', 'DB 已寫入'
|
||||
else:
|
||||
db_status, db_label = 'planned', '待紀錄'
|
||||
db_status, db_label = 'planned', '待 DB 寫入'
|
||||
|
||||
if valid_ppt and preview_cached:
|
||||
preview_status, preview_label = 'ready', 'PDF 快取'
|
||||
|
||||
@@ -20,6 +20,7 @@ from config import BASE_DIR, EXCEL_EXPORT_DIR
|
||||
from database.manager import DatabaseManager
|
||||
from database.models import Product, PriceRecord
|
||||
from services.exporter import Exporter
|
||||
from services.ai_exception_contract import action_requires_ai_exception
|
||||
from services.logger_manager import SystemLogger
|
||||
from utils.momo_url_utils import build_momo_product_url, normalize_momo_product_url
|
||||
|
||||
@@ -94,7 +95,7 @@ def _flatten_review_decision_envelope(item):
|
||||
'決策等級': envelope.get('severity') or '',
|
||||
'決策建議代碼': recommended_action.get('action') or '',
|
||||
'決策責任人': recommended_action.get('owner') or '',
|
||||
'需人工覆核': '是' if recommended_action.get('requires_hitl') else '否',
|
||||
'需 AI 例外決策': '是' if action_requires_ai_exception(recommended_action) else '否',
|
||||
'資料品質': guardrails.get('data_quality') or '',
|
||||
'自動執行允許': '是' if guardrails.get('can_auto_execute') else '否',
|
||||
'自動執行阻擋原因': guardrails.get('blocked_reason') or '',
|
||||
|
||||
@@ -57,7 +57,7 @@ def _humanize_targeted_review_reasons(candidate: dict) -> list[str]:
|
||||
label = reason_map.get(str(reason or "").strip())
|
||||
if label and label not in labels:
|
||||
labels.append(label)
|
||||
return labels or ["需人工確認同款"]
|
||||
return labels or ["需 AI 自動驗證確認同款"]
|
||||
|
||||
|
||||
def _present_momo_review_candidate(candidate: dict) -> dict:
|
||||
|
||||
@@ -23,7 +23,7 @@ sys_log = SystemLogger("AgentAction").get_logger()
|
||||
# ─── Module-level 狀態(記憶體,container restart 清空)─────────────────
|
||||
# 靜音表:event_key → 靜音到期時間
|
||||
_silence_table: dict[str, datetime] = {}
|
||||
# 暫停表:task_name → 暫停到期時間(Phase 4 L3 HITL Ops)
|
||||
# 暫停表:task_name → 暫停到期時間(Phase 4 L3 AI 例外決策 Ops)
|
||||
_paused_tasks: dict[str, datetime] = {}
|
||||
# Retry 狀態:task_name → {attempts, last_ts, last_error}(指數退避用)
|
||||
_retry_state: dict[str, dict] = {}
|
||||
@@ -229,11 +229,11 @@ def is_silenced(event_key: str) -> bool:
|
||||
# 🏷️ 三個既有 NemoTron tool 的 wrapper(供 event_router 統一調用)
|
||||
# =====================================================================
|
||||
def flag_for_human_review(sku: str, concern: str) -> dict:
|
||||
"""升級到 L3 HITL:寫入 human_review 記憶,等待人工後續處理。"""
|
||||
"""升級到 L3 AI 例外決策:寫入 human_review 相容記憶,等待 AI 例外後續處理。"""
|
||||
t0 = time.time()
|
||||
insight_id = _store_action_memory(
|
||||
"human_review",
|
||||
f"SKU={sku} 需要人工覆核:{concern}",
|
||||
f"SKU={sku} 需要 AI 例外決策:{concern}",
|
||||
product_sku=sku,
|
||||
metadata={"sku": sku, "concern": concern, "source": "flag_for_human_review"},
|
||||
status="pending",
|
||||
@@ -274,7 +274,7 @@ def mark_for_relearn(sku: str, reason: str) -> dict:
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# 🛑 L3 OPS Actions(只由 Telegram HITL Callback 觸發,不進 SAFE_ACTIONS)
|
||||
# 🛑 L3 OPS Actions(只由 Telegram AI 例外決策 Callback 觸發,不進 SAFE_ACTIONS)
|
||||
# =====================================================================
|
||||
def pause_task(task_name: str, duration_min: int = 60, operator: str = "unknown") -> dict:
|
||||
"""暫停 scheduler 某個 task 指定分鐘。run_scheduler 須在執行前呼叫 is_task_paused 檢查。"""
|
||||
@@ -320,7 +320,7 @@ def is_task_paused(task_name: str) -> bool:
|
||||
|
||||
|
||||
def force_retry_now(task_name: str, operator: str = "unknown") -> dict:
|
||||
"""HITL:立即強制重試,繞過 backoff,不計入 attempts"""
|
||||
"""AI 例外決策:立即強制重試,繞過 backoff,不計入 attempts"""
|
||||
t0 = time.time()
|
||||
if task_name not in ALLOWED_RETRY_TASKS:
|
||||
result = {"status": "rejected", "reason": "task not in whitelist"}
|
||||
@@ -354,7 +354,7 @@ SAFE_ACTIONS: dict[str, Any] = {
|
||||
"mark_for_relearn": mark_for_relearn,
|
||||
}
|
||||
|
||||
# L3 白名單(僅 Telegram HITL callback 可呼叫,狀態變更類)
|
||||
# L3 白名單(僅 Telegram AI 例外決策 callback 可呼叫,狀態變更類)
|
||||
OPS_ACTIONS: dict[str, Any] = {
|
||||
"pause_task": pause_task,
|
||||
"resume_task": resume_task,
|
||||
|
||||
@@ -457,15 +457,15 @@ def _elephant_hitl_check() -> Dict[str, Any]:
|
||||
api_key_configured = bool(os.getenv("OPENROUTER_API_KEY") or os.getenv("NVIDIA_API_KEY"))
|
||||
if not has_hitl or not has_timeout_guard:
|
||||
status = "critical"
|
||||
summary = "ElephantAlpha HITL 或 timeout guard 缺失"
|
||||
summary = "ElephantAlpha AI 例外決策或 timeout guard 缺失"
|
||||
elif not api_key_configured:
|
||||
status = "warning"
|
||||
summary = "ElephantAlpha HITL 程式可用,但 API key 未設定"
|
||||
summary = "ElephantAlpha AI 例外決策程式可用,但 API key 未設定"
|
||||
else:
|
||||
status = "ok"
|
||||
summary = "ElephantAlpha HITL 與 timeout guard 可用"
|
||||
summary = "ElephantAlpha AI 例外決策與 timeout guard 可用"
|
||||
return _check(
|
||||
"ElephantAlpha HITL",
|
||||
"ElephantAlpha AI 例外決策",
|
||||
status,
|
||||
summary,
|
||||
{
|
||||
@@ -475,7 +475,7 @@ def _elephant_hitl_check() -> Dict[str, Any]:
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
return _check("ElephantAlpha HITL", "critical", f"ElephantAlpha smoke 失敗:{exc}")
|
||||
return _check("ElephantAlpha AI 例外決策", "critical", f"ElephantAlpha smoke 失敗:{exc}")
|
||||
|
||||
|
||||
def collect_ai_automation_smoke(*, record_history: bool = True, history_limit: int = 20) -> Dict[str, Any]:
|
||||
|
||||
@@ -80,7 +80,7 @@ except Exception:
|
||||
|
||||
# 允許 Aider 修改的路徑(正規表示式)
|
||||
# ADR-020 白名單允許 services/routes/database 底下的 Python 模組,含子目錄;
|
||||
# tests/docs/config 等檔案仍需人工處理,避免 Aider 以「修測試」掩蓋產品問題。
|
||||
# tests/docs/config 等檔案仍需 AI 例外處理,避免 Aider 以「修測試」掩蓋產品問題。
|
||||
ALLOWED_FILE_PATTERN = re.compile(
|
||||
r"^(services|routes|database)/(?:[a-zA-Z0-9_]+/)*[a-zA-Z0-9_]+\.py$"
|
||||
)
|
||||
@@ -237,7 +237,7 @@ def execute_code_fix(
|
||||
f"⚠️ <b>AiderHeal 已略過自動修復</b>\n"
|
||||
f"├ 檔案:<code>{html.escape(target_file[:200])}</code>\n"
|
||||
f"├ 原因:不在 ADR-020 自動修復白名單(僅允許 services/routes/database 內 Python 檔案)\n"
|
||||
f"└ 動作:請人工確認 finding,或調整白名單後重跑 Code Review"
|
||||
f"└ 動作:請 AI 自動驗證確認 finding,或調整白名單後重跑 Code Review"
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
@@ -383,11 +383,11 @@ def execute_code_fix(
|
||||
)
|
||||
msg = (
|
||||
f"[AiderHeal] diff 超出限制 {diff_lines} > {MAX_DIFF_LINES} 行,"
|
||||
f"已丟棄,需人工介入"
|
||||
f"已丟棄,需 AI 例外介入"
|
||||
)
|
||||
logger.warning("event=diff_too_large file=%s diff_lines=%d", target_file, diff_lines)
|
||||
_notify_telegram(
|
||||
f"⚠️ <b>AiderHeal:diff 過大,需人工審核</b>\n"
|
||||
f"⚠️ <b>AiderHeal:diff 過大,需 AI 例外決策</b>\n"
|
||||
f"├ 檔案:<code>{target_file}</code>\n"
|
||||
f"├ diff:{diff_lines} 行(上限 {MAX_DIFF_LINES})\n"
|
||||
f"└ 錯誤:<code>{error_type}</code>"
|
||||
@@ -481,7 +481,7 @@ def execute_code_fix(
|
||||
f"🔄 <b>AiderHeal 自動回滾</b>\n"
|
||||
f"├ 原 commit:<code>{commit_sha}</code>\n"
|
||||
f"├ 回滾 commit:<code>{revert_sha}</code>\n"
|
||||
f"└ 需人工排查:<code>{error_type}</code> in <code>{target_file}</code>"
|
||||
f"└ 需 AI 例外排查:<code>{error_type}</code> in <code>{target_file}</code>"
|
||||
)
|
||||
else:
|
||||
msg = f"[AiderHeal] 回滾失敗!需立即人工介入:{revert_err}"
|
||||
|
||||
@@ -899,7 +899,7 @@ class CodeReviewPipeline:
|
||||
"human_review_needed": false
|
||||
}}
|
||||
|
||||
規則(依 ADR-020 全自動修復政策,覆寫 ADR-012 L3 HITL 對 code review 的限制):
|
||||
規則(依 ADR-020 全自動修復政策,覆寫 ADR-012 L3 AI 例外決策對 code review 的限制):
|
||||
- 任何 finding(不論 CRITICAL/HIGH/MEDIUM/LOW)→ auto_fix=true,human_review_needed=false
|
||||
- 安全網是 Git revert + Gitea CI/CD 回滾,不依賴人工審查門檻
|
||||
- priority 按最嚴重 severity 決定:CRITICAL>HIGH>MEDIUM>LOW
|
||||
@@ -1155,7 +1155,7 @@ class CodeReviewPipeline:
|
||||
if auto_fix and allowed_fix_files:
|
||||
fix_status = "🔧 已觸發自動修復(AiderHeal)"
|
||||
elif auto_fix and fix_files:
|
||||
fix_status = "⚠️ 不在自動修復白名單,需人工處理"
|
||||
fix_status = "⚠️ 不在自動修復白名單,需 AI 例外處理"
|
||||
elif sev['critical'] + sev['high'] + sev['medium'] + sev['low'] == 0:
|
||||
fix_status = "✅ 無需修復動作"
|
||||
else:
|
||||
|
||||
@@ -31,6 +31,7 @@ from typing import Dict, List, Any, Optional
|
||||
from sqlalchemy import text
|
||||
from services.logger_manager import SystemLogger
|
||||
from services.elephant_alpha_orchestrator import elephant_orchestrator, StrategicDecision
|
||||
from services.ai_exception_contract import LEGACY_REVIEW_GATE_KEY
|
||||
from database.manager import get_db_manager, get_session
|
||||
|
||||
logger = SystemLogger("ElephantAlphaEngine").get_logger()
|
||||
@@ -45,7 +46,7 @@ COMPETITOR_MATCH_TYPE_LABELS = {
|
||||
COMPETITOR_PRICE_BASIS_LABELS = {
|
||||
"total_price": "總價可比",
|
||||
"unit_price": "單位價可比",
|
||||
"manual_review": "人工覆核後可比",
|
||||
"manual_review": "AI 例外決策後可比",
|
||||
"none": "不可比",
|
||||
}
|
||||
COMPETITOR_ALERT_TIER_LABELS = {
|
||||
@@ -143,7 +144,7 @@ _PRICE_ADJUSTMENT_REVIEW_ACTIONS = frozenset({
|
||||
"dispatch_price_updates",
|
||||
})
|
||||
|
||||
# A' 軌:價格相關觸發類型,HITL 前需 pre-fetch Hermes 具體威脅清單
|
||||
# A' 軌:價格相關觸發類型,AI 例外決策前需 pre-fetch Hermes 具體威脅清單
|
||||
# 取代 Gemini plan 階段的元流程文字(「步驟 1:[OpenClaw] 生成策略」這類)
|
||||
_PRICE_RELATED_TRIGGERS = frozenset({
|
||||
"price_drop_alert",
|
||||
@@ -717,13 +718,13 @@ class ElephantAlphaAutonomousEngine:
|
||||
|
||||
if decision.confidence >= (0.85 if trigger.trigger_type in {"price_drop_alert", "market_opportunity"} else self.confidence_threshold):
|
||||
if trigger.trigger_type in _PRICE_RELATED_TRIGGERS:
|
||||
# 價格類決策即使信心高,也只進 HITL 覆核通知;不得執行
|
||||
# 價格類決策即使信心高,也只進 AI 例外決策覆核通知;不得執行
|
||||
# orchestrator 給出的 Hermes/NemoTron/OpenClaw 長任務 step。
|
||||
# 這避免 scheduler 被 60s execution timeout 卡住,也避免自動調價。
|
||||
await self._notify_telegram_executed(decision, trigger)
|
||||
self._store_escalation(trigger.trigger_type)
|
||||
self._log.info(
|
||||
"Price decision queued for HITL review; execution plan skipped: %s",
|
||||
"Price decision queued for AI 例外決策 review; execution plan skipped: %s",
|
||||
trigger.trigger_type,
|
||||
)
|
||||
self._circuit_reset()
|
||||
@@ -1240,10 +1241,11 @@ class ElephantAlphaAutonomousEngine:
|
||||
},
|
||||
"evidence": evidence,
|
||||
"recommended_action": {
|
||||
"action": "human_review_backlog_triage",
|
||||
"action": "ai_exception_backlog_triage",
|
||||
"owner": "ops",
|
||||
"deadline": deadline,
|
||||
"requires_hitl": True,
|
||||
LEGACY_REVIEW_GATE_KEY: False,
|
||||
"requires_ai_exception": True,
|
||||
},
|
||||
"expected_impact": {
|
||||
"risk_reduction": (
|
||||
@@ -1256,7 +1258,7 @@ class ElephantAlphaAutonomousEngine:
|
||||
"can_auto_execute": False,
|
||||
"blocked_reason": (
|
||||
"resource_optimization 只允許清理過期 advisory action_plans;"
|
||||
"外部修復、價格分析與策略派發需人工覆核"
|
||||
"外部修復、價格分析與策略派發需 AI 例外決策"
|
||||
),
|
||||
"data_quality": data_quality,
|
||||
"llm_used": False,
|
||||
@@ -1292,7 +1294,7 @@ class ElephantAlphaAutonomousEngine:
|
||||
load_judgement = (
|
||||
"主機 CPU 已達高負載門檻。"
|
||||
if metrics.get("load_pressure")
|
||||
else "主機 CPU 未達高負載門檻,這不是主機資源耗盡,而是工作隊列/人工審核積壓。"
|
||||
else "主機 CPU 未達高負載門檻,這不是主機資源耗盡,而是工作隊列/AI 例外決策積壓。"
|
||||
)
|
||||
handling_notes = [
|
||||
f"已寫入 ai_insights(resource_pressure) #{insight_id}。"
|
||||
@@ -1496,7 +1498,7 @@ class ElephantAlphaAutonomousEngine:
|
||||
action = "建議加強曝光或列入 AI 挑品,不需降價"
|
||||
impact = f"MOMO 每件價格優勢 NT$ {gap_amount:,.0f}"
|
||||
else:
|
||||
action = "建議人工確認 PChome identity_v2 後評估跟價或促銷"
|
||||
action = "建議 AI 自動驗證確認 PChome identity_v2 後評估跟價或促銷"
|
||||
impact = f"每件價差 NT$ {gap_amount:,.0f}"
|
||||
|
||||
parts = [
|
||||
@@ -1521,7 +1523,7 @@ class ElephantAlphaAutonomousEngine:
|
||||
return None
|
||||
|
||||
def _fetch_recent_competitor_evidence_actions(self, top_n: int = 5) -> Optional[List[str]]:
|
||||
"""用最新 DB 價差產生 EA HITL 實證,不啟動完整 Hermes LLM。"""
|
||||
"""用最新 DB 價差產生 EA AI 例外決策實證,不啟動完整 Hermes LLM。"""
|
||||
session = get_session()
|
||||
try:
|
||||
rows = session.execute(
|
||||
@@ -1594,7 +1596,7 @@ class ElephantAlphaAutonomousEngine:
|
||||
)
|
||||
|
||||
async def _fetch_hermes_threats_summary(self, top_n: int = 5) -> Optional[List[str]]:
|
||||
"""A' 軌:HITL escalation 前 pre-fetch Hermes 具體威脅清單,
|
||||
"""A' 軌:AI 例外決策 escalation 前 pre-fetch Hermes 具體威脅清單,
|
||||
將「步驟 1: [OpenClaw] 生成策略」這類元流程文字換成
|
||||
「[SKU] 商品|MOMO $X / PChome $Y|流失 NT$ Z|建議 NT$ W」具體可決策行動。
|
||||
|
||||
@@ -1602,7 +1604,7 @@ class ElephantAlphaAutonomousEngine:
|
||||
本方法為 best-effort:任何例外都不阻斷 escalation 主流程。
|
||||
|
||||
Critic High-1 fix: 加 5 秒短超時防止阻塞 escalation cooldown 視窗
|
||||
(Hermes 完整 run 可能 30-60s,HITL 訊息應快速送出)
|
||||
(Hermes 完整 run 可能 30-60s,AI 例外決策訊息應快速送出)
|
||||
Critic High-2 fix: 若每筆都缺 loss/rec_price,視同無料、return None 觸發 fallback
|
||||
"""
|
||||
db_actions = self._fetch_recent_competitor_evidence_actions(top_n=top_n)
|
||||
@@ -1620,7 +1622,7 @@ class ElephantAlphaAutonomousEngine:
|
||||
return None
|
||||
|
||||
# 使用 5s 短超時:Hermes 熱駐留時實測 < 10s,但若需冷啟動會拖到 30s+
|
||||
# HITL 訊息延遲不可大於 10s(影響統帥決策時效性),寧可 fallback 到原 plan 文字
|
||||
# AI 例外決策訊息延遲不可大於 10s(影響統帥決策時效性),寧可 fallback 到原 plan 文字
|
||||
try:
|
||||
result = await asyncio.wait_for(self._hermes_analyze(), timeout=5)
|
||||
except asyncio.TimeoutError:
|
||||
@@ -1721,7 +1723,7 @@ class ElephantAlphaAutonomousEngine:
|
||||
def _record_price_adjustment_review(self, step: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Price changes are business-critical. Elephant Alpha may recommend them,
|
||||
but this system records the proposal for HITL review instead of applying it.
|
||||
but this system records the proposal for AI 例外決策 review instead of applying it.
|
||||
"""
|
||||
params = step.get("parameters") or step.get("params") or {}
|
||||
sku = (
|
||||
@@ -1734,7 +1736,7 @@ class ElephantAlphaAutonomousEngine:
|
||||
action = step.get("action", "price_adjustment")
|
||||
content = (
|
||||
f"[Elephant Alpha 價格調整覆核] AI 建議執行 {action},"
|
||||
f"商品 {sku} 已攔截直接執行並轉入人工審核。"
|
||||
f"商品 {sku} 已攔截直接執行並轉入 AI 例外決策。"
|
||||
)
|
||||
|
||||
session = get_session()
|
||||
@@ -1768,7 +1770,7 @@ class ElephantAlphaAutonomousEngine:
|
||||
enqueue_insight_embedding(insight_id, "human_review", content)
|
||||
except Exception as embed_err:
|
||||
self._log.warning("Embedding enqueue failed for price adjustment review: %s", embed_err)
|
||||
self._log.warning("Price adjustment intercepted for HITL review: action=%s sku=%s", action, sku)
|
||||
self._log.warning("Price adjustment intercepted for AI 例外決策 review: action=%s sku=%s", action, sku)
|
||||
return {"status": "pending_review", "insight_id": insight_id, "sku": sku, "action": action}
|
||||
except Exception:
|
||||
session.rollback()
|
||||
@@ -1830,7 +1832,7 @@ class ElephantAlphaAutonomousEngine:
|
||||
"type": "confidence",
|
||||
"metric": "decision_confidence",
|
||||
"value": f"{float(decision.confidence or 0):.2f}",
|
||||
"basis": "ElephantAlpha high-confidence price signal; HITL still required",
|
||||
"basis": "ElephantAlpha high-confidence price signal; AI 例外決策 still required",
|
||||
"confidence": float(decision.confidence or 0),
|
||||
},
|
||||
{
|
||||
@@ -1854,7 +1856,7 @@ class ElephantAlphaAutonomousEngine:
|
||||
"source_agent": "elephant_alpha",
|
||||
"severity": "P2",
|
||||
"confidence": float(decision.confidence or 0),
|
||||
"analysis": "已找到價格比對實證,轉人工覆核;未批准前不執行調價或外部修復。",
|
||||
"analysis": "已找到價格比對實證,轉 AI 例外決策;未批准前不執行調價或外部修復。",
|
||||
"subject": {
|
||||
"sku": trigger.trigger_type,
|
||||
"name": f"Elephant Alpha · {trigger_label}",
|
||||
@@ -1863,14 +1865,15 @@ class ElephantAlphaAutonomousEngine:
|
||||
"recommended_action": {
|
||||
"action": "review_price_or_promo",
|
||||
"owner": "ops",
|
||||
"requires_hitl": True,
|
||||
LEGACY_REVIEW_GATE_KEY: False,
|
||||
"requires_ai_exception": True,
|
||||
},
|
||||
"expected_impact": {
|
||||
"risk_reduction": "prevent unverified automated price action while preserving actionable evidence",
|
||||
},
|
||||
"guardrails": {
|
||||
"can_auto_execute": False,
|
||||
"blocked_reason": "price decisions require HITL; execution_plan skipped",
|
||||
"blocked_reason": "price decisions require AI 例外決策; execution_plan skipped",
|
||||
"data_quality": "complete" if concrete_actions else "missing",
|
||||
},
|
||||
"trace": {
|
||||
@@ -1906,32 +1909,32 @@ class ElephantAlphaAutonomousEngine:
|
||||
"event_type": "ea_price_review",
|
||||
"title": f"🐘 EA 價格覆核 · {_zh_trigger(trigger.trigger_type)}",
|
||||
"summary": (
|
||||
f"找到 {len(concrete_actions)} 筆價格比對實證,已轉人工覆核;"
|
||||
f"找到 {len(concrete_actions)} 筆價格比對實證,已轉 AI 例外決策;"
|
||||
"未批准前不自動調價。"
|
||||
),
|
||||
"id": decision_envelope.get("decision_id"),
|
||||
"decision_envelope": decision_envelope,
|
||||
},
|
||||
tier_label="🐘 Elephant Alpha · L3 HITL",
|
||||
tier_label="🐘 Elephant Alpha · L3 AI 例外決策",
|
||||
ai_summary=(
|
||||
f"已保留 {len(concrete_actions)} 筆 DB/Hermes 價格比對實證;"
|
||||
"本通知只要求人工覆核,不執行外部修復或調價。"
|
||||
"本通知只要求 AI 例外決策,不執行外部修復或調價。"
|
||||
),
|
||||
ai_cause=(
|
||||
f"觸發類型:{_zh_trigger(trigger.trigger_type)} | "
|
||||
f"信心度:{decision.confidence:.2f} | "
|
||||
"高信心價格訊號仍需 HITL"
|
||||
"高信心價格訊號仍需 AI 例外決策"
|
||||
),
|
||||
ai_actions=concrete_actions,
|
||||
)
|
||||
await self._run_with_timeout(_send_telegram_raw, msg, timeout=10, reply_markup=keyboard)
|
||||
self._log.info(
|
||||
"Price HITL review Telegram sent: %s concrete=%d",
|
||||
"Price AI 例外決策 review Telegram sent: %s concrete=%d",
|
||||
trigger.trigger_type,
|
||||
len(concrete_actions),
|
||||
)
|
||||
except Exception as e:
|
||||
self._log.error("Price HITL review Telegram failed (non-blocking): %s", e)
|
||||
self._log.error("Price AI 例外決策 review Telegram failed (non-blocking): %s", e)
|
||||
|
||||
@staticmethod
|
||||
def _get_prefetched_concrete_actions(trigger: AutonomousTrigger) -> Optional[List[str]]:
|
||||
@@ -2033,23 +2036,24 @@ class ElephantAlphaAutonomousEngine:
|
||||
"source_agent": "elephant_alpha",
|
||||
"severity": "P2" if data_quality == "complete" else "P3",
|
||||
"confidence": float(decision.confidence or 0),
|
||||
"analysis": "低信心自主決策已轉人工覆核;未批准前不執行外部副作用。",
|
||||
"analysis": "低信心自主決策已轉 AI 例外決策;未批准前不執行外部副作用。",
|
||||
"subject": {
|
||||
"sku": trigger.trigger_type,
|
||||
"name": f"Elephant Alpha · {trigger_label}",
|
||||
},
|
||||
"evidence": evidence,
|
||||
"recommended_action": {
|
||||
"action": "human_review",
|
||||
"action": "ai_exception_decision",
|
||||
"owner": "ops",
|
||||
"requires_hitl": True,
|
||||
LEGACY_REVIEW_GATE_KEY: False,
|
||||
"requires_ai_exception": True,
|
||||
},
|
||||
"expected_impact": {
|
||||
"risk_reduction": "prevent low-confidence autonomous execution",
|
||||
},
|
||||
"guardrails": {
|
||||
"can_auto_execute": False,
|
||||
"blocked_reason": "L3 HITL required; no automatic execution before approval",
|
||||
"blocked_reason": "L3 AI 例外決策 required; no automatic execution before approval",
|
||||
"data_quality": data_quality,
|
||||
},
|
||||
"trace": {
|
||||
@@ -2176,13 +2180,13 @@ class ElephantAlphaAutonomousEngine:
|
||||
"event_type": "ea_escalation",
|
||||
"title": f"🐘 EA 升級審核 · {_zh_trigger(trigger.trigger_type)}",
|
||||
"summary": (
|
||||
f"自主決策信心度 {decision.confidence:.2f} 低於門檻,需人工批准"
|
||||
f"自主決策信心度 {decision.confidence:.2f} 低於門檻,需 AI 例外決策"
|
||||
+ ("" if concrete_actions else "(⚠️ 無實證數據)")
|
||||
),
|
||||
"id": decision_envelope.get("decision_id"),
|
||||
"decision_envelope": decision_envelope,
|
||||
},
|
||||
tier_label="🐘 Elephant Alpha · L3 HITL",
|
||||
tier_label="🐘 Elephant Alpha · L3 AI 例外決策",
|
||||
ai_summary=ai_summary_text,
|
||||
ai_cause=ai_cause_text,
|
||||
ai_actions=ai_actions_payload,
|
||||
|
||||
@@ -165,7 +165,7 @@ OpenClaw strategy/report actions are advisory only. Do not place openclaw / gene
|
||||
價格調整紅線:
|
||||
- 禁止輸出 execute_price_adjustment、adjust_price、apply_price_change、update_price。
|
||||
- 若需要調價,請在 strategic_assessment / reasoning 中輸出定價策略建議,並明確說明「需要人工核准後才能調價」;不要放入 execution_plan。
|
||||
- 本系統不得由 AI 直接修改商品價格,只能產生建議與人工覆核項目。
|
||||
- 本系統不得由 AI 直接修改商品價格,只能產生建議與 AI 例外決策項目。
|
||||
|
||||
BUSINESS CONTEXT:
|
||||
- E-commerce platform (momo-pro-system)
|
||||
@@ -508,14 +508,14 @@ Provide your strategic decision in the specified JSON format.
|
||||
agents_required=["hermes", "elephant_alpha"],
|
||||
reasoning=(
|
||||
f"{reason} 已保留 {len(concrete_actions)} 筆 DB/Hermes 價格比對實證;"
|
||||
"僅送人工覆核,不執行自動調價。"
|
||||
"僅送AI 例外決策,不執行自動調價。"
|
||||
),
|
||||
expected_outcome="產生可稽核的人工覆核告警,避免使用無法解析的 LLM 推論文字。",
|
||||
expected_outcome="產生可稽核的 AI 例外決策告警,避免使用無法解析的 LLM 推論文字。",
|
||||
confidence=0.74,
|
||||
execution_plan=[],
|
||||
resource_requirements={
|
||||
"compute_cost": "$0.00",
|
||||
"time_estimate": "人工覆核",
|
||||
"time_estimate": "AI 例外決策",
|
||||
"human_oversight": "required",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -560,7 +560,7 @@ def _build_telegram_message(event: Dict[str, Any], tier: str, result: Optional[D
|
||||
if isinstance(result, dict):
|
||||
ai_summary = str(result.get("summary") or "")
|
||||
if not ai_summary:
|
||||
ai_summary = str(decision_envelope.get("analysis") or "依決策信封進行人工覆核。")
|
||||
ai_summary = str(decision_envelope.get("analysis") or "依決策信封進行 AI 例外決策。")
|
||||
return triaged_alert(
|
||||
render_event,
|
||||
tier_label=f"{source_agent} · {severity}",
|
||||
|
||||
@@ -133,10 +133,10 @@ class HermesAnalystService:
|
||||
只能基於 gap_pct、sales_delta、competitor_tags 等已提供欄位做推論。
|
||||
6. 【身份證據鐵律】match_type / price_basis / alert_tier 是系統比對結論,不得改寫。
|
||||
- 只有 alert_tier="price_alert_exact" 且 match_type="exact" 且 price_basis="total_price" 可當成直接價格威脅。
|
||||
- 其他情況只能建議「人工覆核身份/包裝/單位價」,不可建議直接降價。
|
||||
- 其他情況只能建議「AI 例外決策身份/包裝/單位價」,不可建議直接降價。
|
||||
7. 【非價格異常路由】若 gap_pct 絕對值 < 5% 但 sales_delta < -30%:
|
||||
- 判定為「非價格因素異常」(高機率:缺貨、下架、平台流量異常、頁面問題)
|
||||
- risk 設為 MED,recommended_action 必須寫「價差接近零但業績異常下滑,建議立即人工走查前台頁面(確認是否缺貨/下架/頁面異常)」
|
||||
- risk 設為 MED,recommended_action 必須寫「價差接近零但業績異常下滑,建議立即AI 例外走查前台頁面(確認是否缺貨/下架/頁面異常)」
|
||||
- confidence 設為 0.5(因缺乏確切原因)"""
|
||||
|
||||
def __init__(self, engine=None):
|
||||
@@ -560,7 +560,7 @@ class HermesAnalystService:
|
||||
f"分析以下 {len(items_for_llm)} 支商品的競價威脅,回傳前 {TOP_N} 個最高風險商品。\n\n"
|
||||
f"注意:match_type / price_basis / alert_tier 是比對系統的硬證據;"
|
||||
f"只有 alert_tier=price_alert_exact 的 exact 同款可判定直接價格威脅,"
|
||||
f"其他一律只能建議人工覆核身份、包裝或單位價。\n\n"
|
||||
f"其他一律只能建議AI 例外決策身份、包裝或單位價。\n\n"
|
||||
f"資料:{json.dumps(items_for_llm, ensure_ascii=False)}\n\n"
|
||||
f"輸出格式(JSON 陣列,每筆含):\n"
|
||||
f'[{{"sku": string, "name": string, "category": string, '
|
||||
|
||||
@@ -14,7 +14,7 @@ Owen 強調的 v5.0 護欄 #1(ADR-033):
|
||||
Stage 1: quality_score >= 0.7
|
||||
Stage 2: 規則引擎幻覺檢測
|
||||
Stage 3: 與既有 insight cosine < 0.95(去重)
|
||||
Stage 4: weight >= 0.8 必經 Telegram 👍/👎 人工驗收(24h 無回應降級 0.5)
|
||||
Stage 4: weight >= 0.8 必經 Telegram 👍/👎 AI 驗證驗收(24h 無回應降級 0.5)
|
||||
|
||||
對應:
|
||||
- migrations/028_create_learning_episodes.sql
|
||||
@@ -39,7 +39,7 @@ logger = logging.getLogger(__name__)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
STAGE_1_AUTO_QUALITY = 0.7 # quality_score 下限
|
||||
STAGE_3_DEDUP_THRESHOLD = 0.95 # cosine similarity 視為重複
|
||||
STAGE_4_HUMAN_REVIEW_WEIGHT = 0.8 # weight >= 此值強制人工驗收
|
||||
STAGE_4_HUMAN_REVIEW_WEIGHT = 0.8 # weight >= 此值強制 AI 驗證驗收
|
||||
HUMAN_REVIEW_TIMEOUT_HOURS = 24 # awaiting_review 過期門檻
|
||||
EXPIRED_FALLBACK_WEIGHT = 0.5 # 過期降級後的 weight
|
||||
|
||||
@@ -56,7 +56,7 @@ _HALLUCINATION_HEDGE_WORDS = ['我猜', '可能是', '也許', '大概是', '應
|
||||
_NUMBER_PATTERN = re.compile(r'\d')
|
||||
# 「自相矛盾」啟發式:簡化判斷 — 同一句裡同一主詞同時被指派不同值(v5.0 不深做語意,rule-only)
|
||||
|
||||
# user_feedback 蒸餾後預設 weight(高權重,必入 Stage 4 人工驗收)
|
||||
# user_feedback 蒸餾後預設 weight(高權重,必入 Stage 4 AI 驗證驗收)
|
||||
_USER_FEEDBACK_DEFAULT_WEIGHT = 0.9
|
||||
_MANUAL_CURATED_DEFAULT_WEIGHT = 1.0
|
||||
|
||||
@@ -139,7 +139,7 @@ class Distiller:
|
||||
score = max(1, min(int(user_feedback_score or 3), 5))
|
||||
# 5 → 1.0 高品質;1 → 0.0 負樣本
|
||||
quality = round((score - 1) / 4.0, 3)
|
||||
weight = _USER_FEEDBACK_DEFAULT_WEIGHT # 用戶直陳的事實,需人工驗收
|
||||
weight = _USER_FEEDBACK_DEFAULT_WEIGHT # 用戶直陳的事實,需 AI 驗證驗收
|
||||
|
||||
elif et == 'mcp_result':
|
||||
# MCP:>200 字 + 含 2+ 關鍵字 → 0.8;否則 0.5
|
||||
@@ -302,7 +302,7 @@ class LearningPipeline:
|
||||
class PromotionGate:
|
||||
"""learning_episodes → ai_insights 4 階段晉升閘。
|
||||
|
||||
Owen 強調:高權重必經人工驗收,避免幻覺污染 RAG。
|
||||
Owen 強調:高權重必經AI 驗證驗收,避免幻覺污染 RAG。
|
||||
|
||||
使用範例:
|
||||
gate = PromotionGate()
|
||||
@@ -417,7 +417,7 @@ class PromotionGate:
|
||||
return None
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Stage 4: 高權重強制人工驗收
|
||||
# Stage 4: 高權重強制 AI 驗證驗收
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
def _stage_4_review(self, episode: Dict[str, Any]) -> GateDecision:
|
||||
"""weight >= 0.8 → awaiting_review;否則自動晉升 approved。"""
|
||||
@@ -426,7 +426,7 @@ class PromotionGate:
|
||||
return GateDecision(
|
||||
can_promote=False,
|
||||
reason='awaiting_review',
|
||||
detail=f'weight={weight:.3f} >= {STAGE_4_HUMAN_REVIEW_WEIGHT} 強制人工驗收',
|
||||
detail=f'weight={weight:.3f} >= {STAGE_4_HUMAN_REVIEW_WEIGHT} 強制 AI 驗證驗收',
|
||||
)
|
||||
return GateDecision(can_promote=True, reason='approved')
|
||||
|
||||
@@ -543,7 +543,7 @@ class PromotionGate:
|
||||
Args:
|
||||
reason: rejected_quality / rejected_hallucination / rejected_duplicate / rejected_human
|
||||
detail: 補充說明(會與 reason 拼成 rejected_reason 文本)
|
||||
human_approver: 已 hash 後的人工審核者識別;不存 PII 原文。
|
||||
human_approver: 已 hash 後的 AI 例外決策者識別;不存 PII 原文。
|
||||
"""
|
||||
valid_statuses = (
|
||||
'rejected_quality',
|
||||
|
||||
@@ -26,7 +26,7 @@ CALLER_REGISTRY: frozenset = frozenset({
|
||||
# Hermes 競價分析(hermes_analyst_service)
|
||||
'hermes_analyst', # _call_hermes_batch
|
||||
'hermes_intent', # intent_classify (L1 NLP)
|
||||
'hermes_ea_prefetch', # EA HITL pre-fetch (ADR-021)
|
||||
'hermes_ea_prefetch', # EA AI 例外決策 pre-fetch (ADR-021)
|
||||
|
||||
# KM Embedding(openclaw_learning_service)
|
||||
'km_embedding_worker', # 60s retry queue worker
|
||||
|
||||
@@ -80,7 +80,7 @@ ROUTING_RULES: Dict[str, list] = {
|
||||
'minicpm-v:latest'),
|
||||
],
|
||||
|
||||
# 推理增強場景(EA HITL 戰略決策;Gemini 不可作為預設模型)
|
||||
# 推理增強場景(EA AI 例外決策戰略決策;Gemini 不可作為預設模型)
|
||||
'ea_engine': [
|
||||
(lambda ctx: bool(ctx.get('require_chain_of_thought', False)),
|
||||
'deepseek-r1:14b'),
|
||||
|
||||
@@ -14,7 +14,7 @@ NemoTron 行動派發器 (Module 2 — Dispatcher)
|
||||
工具清單(扁平化,避免 NIM JSON 截斷):
|
||||
trigger_price_alert → Telegram 競價高危險預警
|
||||
add_to_recommendation → 寫入前台推薦商品 + Telegram 通知
|
||||
flag_for_human_review → Telegram 人工覆核請求
|
||||
flag_for_human_review → Telegram AI 例外決策請求
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -29,6 +29,7 @@ from services.mcp_context_service import build_mcp_context
|
||||
|
||||
from config import HERMES_URL # ADR-008 集中化:禁止硬編碼 IP
|
||||
from services.ai_call_logger import log_ai_call # Operation Ollama-First v5.0 P1
|
||||
from services.ai_exception_contract import LEGACY_REVIEW_GATE_KEY
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -67,14 +68,14 @@ def _has_repeated_phrase(text: str, min_len: int = 4, min_count: int = 2) -> boo
|
||||
return False
|
||||
|
||||
|
||||
def _sanitize_text(text: str, fallback: str = "請人工確認", max_len: int = 200) -> str:
|
||||
def _sanitize_text(text: str, fallback: str = "請 AI 自動驗證確認", max_len: int = 200) -> str:
|
||||
"""防止 LLM 幻覺文字輸出到 Telegram。
|
||||
|
||||
[2026-04-18 台北] Bug-3.1 三層檢測升級 — Claude Opus 4.7
|
||||
L1: 連續 ≥15 字中文且整段無標點 → 幻覺(原規則)
|
||||
L2: 命中簡體字/異體字黑名單 → 簡繁污染
|
||||
L3: 連續中文片段內 4+ 字子串重複 ≥2 次 → 語意坍塌
|
||||
任一層命中 → fallback 替換為「請人工確認」(或呼叫端指定字串)
|
||||
任一層命中 → fallback 替換為「請 AI 自動驗證確認」(或呼叫端指定字串)
|
||||
"""
|
||||
if not text or not isinstance(text, str):
|
||||
return fallback
|
||||
@@ -208,7 +209,7 @@ TOOLS = [
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "flag_for_human_review",
|
||||
"description": "當情況複雜、AI 信心不足,或需要人工決策時,發送 Telegram 請求人工覆核",
|
||||
"description": "當情況複雜、AI 信心不足,或需要 AI 例外決策時,發送 Telegram 請求AI 例外決策",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -325,7 +326,7 @@ def _compute_business_impact(threat) -> dict:
|
||||
HEADER_DISPATCHER = "⚡ NemoTron 派發器"
|
||||
# 風險級別
|
||||
ICON_CRITICAL = "🚨" # 高危險/立即行動
|
||||
ICON_WARNING = "⚠️" # 中風險/人工覆核
|
||||
ICON_WARNING = "⚠️" # 中風險/AI 例外決策
|
||||
ICON_INSIGHT = "💡" # 低風險/策略建議
|
||||
ICON_REPORT = "📊" # 例行報告
|
||||
# 業務屬性
|
||||
@@ -345,7 +346,7 @@ MATCH_TYPE_LABELS = {
|
||||
PRICE_BASIS_LABELS = {
|
||||
"total_price": "總價可比",
|
||||
"unit_price": "單位價可比",
|
||||
"manual_review": "人工覆核後可比",
|
||||
"manual_review": "AI 例外決策後可比",
|
||||
"none": "不可比",
|
||||
}
|
||||
ALERT_TIER_LABELS = {
|
||||
@@ -528,9 +529,9 @@ def _build_price_decision_envelope(
|
||||
|
||||
action = "price_follow_review" if decision_type == "price_alert" else "identity_or_price_review"
|
||||
blocked_reason = (
|
||||
"價格調整需人工覆核;不得自動寫入或覆蓋正式競品價格"
|
||||
"價格調整需 AI 例外決策;不得自動寫入或覆蓋正式競品價格"
|
||||
if decision_type == "price_alert"
|
||||
else "身份、包裝、單位價或前台狀態需人工確認"
|
||||
else "身份、包裝、單位價或前台狀態需 AI 自動驗證確認"
|
||||
)
|
||||
risk_reduction = "high" if severity == "P1" else ("medium" if severity == "P2" else "watch")
|
||||
|
||||
@@ -549,11 +550,12 @@ def _build_price_decision_envelope(
|
||||
"competitor_product_name": str(competitor_product_name or "")[:120],
|
||||
},
|
||||
"evidence": evidence,
|
||||
"analysis": _sanitize_text(analysis, fallback="請人工確認", max_len=300),
|
||||
"analysis": _sanitize_text(analysis, fallback="請 AI 自動驗證確認", max_len=300),
|
||||
"recommended_action": {
|
||||
"action": action,
|
||||
"owner": "營運",
|
||||
"requires_hitl": True,
|
||||
LEGACY_REVIEW_GATE_KEY: False,
|
||||
"requires_ai_exception": True,
|
||||
},
|
||||
"expected_impact": {
|
||||
"momo_price": momo_value if momo_value > 0 else None,
|
||||
@@ -771,7 +773,7 @@ class NemotronDispatcher:
|
||||
"1. match_type 不是 exact,或 price_basis 不是 total_price,或 alert_tier 不是 price_alert_exact "
|
||||
"→ 不可直接價格告警,呼叫 flag_for_human_review,concern 說明需覆核身份、包裝或單位價。\n"
|
||||
"2. gap_pct < 5% 且 sales_delta < -30% → 非價格異常,呼叫 flag_for_human_review,"
|
||||
"concern 說明『價差接近 0 但銷量大幅下滑,疑似缺貨/下架/平台流量異常,請人工走查前台』。\n"
|
||||
"concern 說明『價差接近 0 但銷量大幅下滑,疑似缺貨/下架/平台流量異常,請 AI 例外走查前台』。\n"
|
||||
"3. gap_pct ≥ 5% 且 risk=HIGH → trigger_price_alert(填入 momo_price, comp_price)。\n"
|
||||
"4. 我方價格低於競品且銷量正成長 → add_to_recommendation。\n"
|
||||
"5. confidence < 0.6 或其他複雜情況 → flag_for_human_review。\n"
|
||||
@@ -918,7 +920,7 @@ class NemotronDispatcher:
|
||||
"1. match_type 不是 exact,或 price_basis 不是 total_price,或 alert_tier 不是 price_alert_exact "
|
||||
"→ 不可直接價格告警,呼叫 flag_for_human_review,concern 說明需覆核身份、包裝或單位價。\n"
|
||||
"2. gap_pct < 5% 且 sales_delta < -30% → 非價格異常,呼叫 flag_for_human_review,"
|
||||
"concern 說明『價差接近 0 但銷量大幅下滑,疑似缺貨/下架/平台流量異常,請人工走查前台』。\n"
|
||||
"concern 說明『價差接近 0 但銷量大幅下滑,疑似缺貨/下架/平台流量異常,請 AI 例外走查前台』。\n"
|
||||
"3. gap_pct ≥ 5% 且 risk=HIGH → trigger_price_alert(填入 momo_price, comp_price)。\n"
|
||||
"4. 我方價格低於競品且銷量正成長 → add_to_recommendation。\n"
|
||||
"5. confidence < 0.6 或其他複雜情況 → flag_for_human_review。\n"
|
||||
@@ -1076,13 +1078,13 @@ class NemotronDispatcher:
|
||||
continue
|
||||
|
||||
if t.gap_pct < 5 and t.sales_7d_delta_pct < -30:
|
||||
# Rule 1:價差微小但銷量大跌 → 非定價問題,人工確認
|
||||
# Rule 1:價差微小但銷量大跌 → 非定價問題,AI 自動驗證確認
|
||||
self._exec_flag_for_human_review(
|
||||
sku=t.sku, name=t.name,
|
||||
concern=(
|
||||
f"🟡 [規則引擎] 價差僅 {t.gap_pct:+.1f}% 但銷量大跌 "
|
||||
f"{t.sales_7d_delta_pct:+.1f}%,疑似缺貨/下架/平台流量異常,"
|
||||
"請人工走查前台。"
|
||||
"請 AI 例外走查前台。"
|
||||
),
|
||||
confidence=0.80,
|
||||
footprint=footprint,
|
||||
@@ -1116,7 +1118,7 @@ class NemotronDispatcher:
|
||||
threat=t,
|
||||
)
|
||||
else:
|
||||
# Rule 4:其餘複雜情況 → 人工覆核
|
||||
# Rule 4:其餘複雜情況 → AI 例外決策
|
||||
self._exec_flag_for_human_review(
|
||||
sku=t.sku, name=t.name,
|
||||
concern=(
|
||||
@@ -1236,7 +1238,7 @@ class NemotronDispatcher:
|
||||
competitor_product_name: str = "",
|
||||
) -> str:
|
||||
"""
|
||||
類別二:人工覆核
|
||||
類別二:AI 例外決策
|
||||
客觀數據由 Python 注入(防幻覺),AI 診斷隔離在獨立欄位
|
||||
B' 軌:補金額影響欄位
|
||||
"""
|
||||
@@ -1268,7 +1270,7 @@ class NemotronDispatcher:
|
||||
)
|
||||
|
||||
return (
|
||||
f"{ICON_WARNING} [{HEADER_DISPATCHER}] 異常波動需人工覆核\n\n"
|
||||
f"{ICON_WARNING} [{HEADER_DISPATCHER}] 異常波動需 AI 例外決策\n\n"
|
||||
f"🔍 待查商品:[{sku}] {name}\n\n"
|
||||
f"{data_block}"
|
||||
f"{match_block}"
|
||||
@@ -1460,7 +1462,7 @@ class NemotronDispatcher:
|
||||
competitor_product_id: str = "",
|
||||
competitor_product_name: str = "",
|
||||
):
|
||||
"""發送語意化人工覆核請求"""
|
||||
"""發送語意化AI 例外決策請求"""
|
||||
concern = _sanitize_text(concern, fallback=f"數據走勢違背常理,疑似缺貨或前台異常。")
|
||||
msg = self._fmt_human_review(
|
||||
sku, name, concern, footprint,
|
||||
@@ -1496,13 +1498,13 @@ class NemotronDispatcher:
|
||||
)
|
||||
self._send_telegram(msg, decision_envelope=decision_envelope)
|
||||
logger.info(
|
||||
f"[Dispatcher] 人工覆核請求 → {sku} loss=${revenue_loss_7d:,.0f}"
|
||||
f"[Dispatcher] AI 例外決策請求 → {sku} loss=${revenue_loss_7d:,.0f}"
|
||||
)
|
||||
# ADR-007 雙寫
|
||||
self._sink_insight_to_km(
|
||||
insight_type="human_review",
|
||||
sku=sku, name=name,
|
||||
content=f"[人工覆核] {name}。疑慮:{concern}",
|
||||
content=f"[AI 例外決策] {name}。疑慮:{concern}",
|
||||
metadata={"confidence": confidence, "gap_pct": gap_pct, "sales_delta": sales_delta,
|
||||
"momo_price": momo_price, "comp_price": comp_price,
|
||||
"revenue_loss_7d": revenue_loss_7d,
|
||||
@@ -1740,7 +1742,7 @@ class NemotronDispatcher:
|
||||
閘門 A:銷量 ≤ -95% 絕對斷崖 → 100% 是缺貨/下架/前台異常,不論價差
|
||||
(2026-04-18 傍晚升級:真實案例 sku=7440662 sales=-100% gap=6.1% 被 NemoTron 錯派降價)
|
||||
閘門 B:銷量 ≤ -80% 且 |價差| < 5% → 中度斷崖 + 價差微小,定價非主因
|
||||
命中任一閘門 → 強制走人工覆核,不進 NIM — Claude Opus 4.7
|
||||
命中任一閘門 → 強制走 AI 例外決策,不進 NIM — Claude Opus 4.7
|
||||
"""
|
||||
if not threats:
|
||||
return {"dispatched": 0, "skipped": 0, "errors": [], "nim_stats": {}}
|
||||
@@ -2102,7 +2104,7 @@ if __name__ == "__main__":
|
||||
recommended_price=980, # B' 軌驗證:跟進競品價
|
||||
))
|
||||
print()
|
||||
print("=== 類別二:人工覆核 ===")
|
||||
print("=== 類別二:AI 例外決策 ===")
|
||||
# [2026-04-18 台北] CLI 測試修正:_fmt_human_review 用 kwargs,避免位置錯位 — Claude Opus 4.7
|
||||
print(NemotronDispatcher._fmt_human_review(
|
||||
sku="A001", name="玻尿酸面膜10片裝",
|
||||
@@ -2145,7 +2147,7 @@ if __name__ == "__main__":
|
||||
forced.append(t)
|
||||
else:
|
||||
nim_list.append(t)
|
||||
print(f"強制人工覆核 {len(forced)} 筆(預期 3:閘門 A + 閘門 B × 2): "
|
||||
print(f"強制 AI 例外決策 {len(forced)} 筆(預期 3:閘門 A + 閘門 B × 2): "
|
||||
f"{[t.sku for t in forced]}")
|
||||
print(f"送入 NIM 決策 {len(nim_list)} 筆(預期 1): "
|
||||
f"{[t.sku for t in nim_list]}")
|
||||
|
||||
@@ -115,7 +115,7 @@ def run_pchome_growth_momo_backfill(
|
||||
"""補高業績 PChome 商品的 MOMO 對應。
|
||||
|
||||
不呼叫 LLM,只搜尋 MOMO 候選,並只把可自動判斷的 total_price / unit_price
|
||||
寫入 external_offers;需人工確認的候選會以 needs_review 保存,不進價格判斷。
|
||||
寫入 external_offers;需 AI 自動驗證確認的候選會以 needs_review 保存,不進價格判斷。
|
||||
"""
|
||||
limit = max(1, min(int(limit or 12), 20))
|
||||
build_payload = build_payload_func or _default_build_payload
|
||||
|
||||
@@ -13,6 +13,8 @@ pip install python-telegram-bot>=20.0
|
||||
- 每日趨勢摘要推播
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
@@ -509,7 +511,7 @@ class TrendTelegramBot:
|
||||
|
||||
await query.answer()
|
||||
|
||||
# ADR-012 / A' 軌:HITL escalation 按鈕(momo:eig:{event_id} = event_ignore)
|
||||
# ADR-012 / A' 軌:AI 例外事件忽略按鈕(momo:eig:{event_id} = event_ignore)
|
||||
# triaged_alert 鍵盤按鈕「🛑 忽略此事件」會 callback momo:eig:<id>
|
||||
if data.startswith('momo:eig:'):
|
||||
await self._handle_event_ignore_callback(query, data)
|
||||
@@ -586,7 +588,7 @@ class TrendTelegramBot:
|
||||
await self._handle_settings_callback(query, data)
|
||||
|
||||
async def _handle_event_ignore_callback(self, query, data: str):
|
||||
"""A' 軌:HITL 升級審核「🛑 忽略此事件」按鈕處理。
|
||||
"""A' 軌:AI 例外決策「🛑 忽略此事件」按鈕處理。
|
||||
|
||||
callback_data 格式:momo:eig:<event_id>
|
||||
動作:
|
||||
@@ -594,7 +596,7 @@ class TrendTelegramBot:
|
||||
2. 編輯原訊息:標記「已忽略 by <user> @ <ts>」
|
||||
3. 失敗 best-effort,不阻斷 user 體驗
|
||||
|
||||
ADR-012 §③:人工決策必有 audit trail;ai_insights 為 SOT。
|
||||
ADR-012 §③:AI 例外決策必有 audit trail;ai_insights 為 SOT。
|
||||
Critic Critical-1 fix: user_label / ts_label 寫入 HTML 前須 escape;
|
||||
Critic Medium-2 fix: 空 event_id 直接拒絕,避免污染 audit 資料。
|
||||
"""
|
||||
@@ -609,7 +611,7 @@ class TrendTelegramBot:
|
||||
await query.answer("event_id 缺失,忽略動作未生效", show_alert=False)
|
||||
except Exception:
|
||||
logger.debug("empty event_id callback answer failed", exc_info=True)
|
||||
logger.warning("[EA HITL] empty event_id callback rejected: %r", data)
|
||||
logger.warning("[EA AI Exception] empty event_id callback rejected: %r", data)
|
||||
return
|
||||
|
||||
user = getattr(query, 'from_user', None)
|
||||
@@ -632,8 +634,8 @@ class TrendTelegramBot:
|
||||
VALUES (:type, :content, :conf, :by, :status, :meta)
|
||||
"""),
|
||||
{
|
||||
"type": "human_review",
|
||||
"content": f"[EA HITL] 事件 {event_id} 由 {user_label_raw} 忽略",
|
||||
"type": "ai_exception_decision",
|
||||
"content": f"[EA AI Exception] 事件 {event_id} 由 {user_label_raw} 忽略",
|
||||
"conf": 1.0,
|
||||
"by": f"telegram:{user_label_raw}",
|
||||
"status": "ignored",
|
||||
@@ -642,6 +644,8 @@ class TrendTelegramBot:
|
||||
"decided_by": user_label_raw,
|
||||
"decided_at": ts_label_raw,
|
||||
"decision": "ignored",
|
||||
"decision_mode": "ai_exception_decision",
|
||||
"legacy_insight_type": "human_review",
|
||||
}, ensure_ascii=False),
|
||||
},
|
||||
)
|
||||
@@ -649,7 +653,7 @@ class TrendTelegramBot:
|
||||
finally:
|
||||
session.close()
|
||||
except Exception as audit_err:
|
||||
logger.warning(f"[EA HITL] ai_insights audit 寫入失敗(不阻斷 UI): {audit_err}")
|
||||
logger.warning(f"[EA AI Exception] ai_insights audit 寫入失敗(不阻斷 UI): {audit_err}")
|
||||
|
||||
# Critic Critical-1: user_label / ts_label 須 HTML escape,避免攻擊者
|
||||
# 透過 Telegram username 注入 <a>/<pre>/破版標籤
|
||||
@@ -663,9 +667,9 @@ class TrendTelegramBot:
|
||||
parse_mode='HTML',
|
||||
)
|
||||
except Exception as ui_err:
|
||||
logger.debug(f"[EA HITL] edit_message 失敗(不阻斷): {ui_err}")
|
||||
logger.debug(f"[EA AI Exception] edit_message 失敗(不阻斷): {ui_err}")
|
||||
|
||||
logger.info(f"[EA HITL] event_ignore event_id={event_id} by={user_label_raw}")
|
||||
logger.info(f"[EA AI Exception] event_ignore event_id={event_id} by={user_label_raw}")
|
||||
|
||||
async def _handle_openclaw_callback(self, query, context, data: str):
|
||||
"""轉接 OpenClaw 完整菜單 callback,避免長輪詢 Bot 吃掉 /menu。"""
|
||||
|
||||
Reference in New Issue
Block a user