Explain protected PChome match conflicts
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.459 強化 PChome `protected_existing_match` 決策封包:解析 `existing_match_conflict` 的既有候選、新候選與雙方 score,寫入 `decision_envelope.evidence` / `expected_impact` / `guardrails`,並把下一步明確標成「比較既有正式候選與新候選」;仍保持 `can_auto_execute=false`,避免新候選分數較高時繞過人工覆核自動覆蓋正式價差。
|
||||
- V10.458 將 OpenClaw / 競品 PPT 接上 PChome 覆核 `decision_envelope` 摘要:`competitor_intel_repository.summarize_review_decision_envelopes()` 成為共用 formatter,OpenClaw 週報/日報/月報與競品簡報 data_summary / KPI slide 都讀同一份信封文字,避免策略報告與 PPT 各自翻譯覆核狀態或遺失 HITL guardrails。
|
||||
- V10.457 將 PChome 覆核 `decision_envelope` 連到人工操作面:Dashboard 覆核卡新增決策等級、資料品質、HITL/trace 信封摘要;`/api/export/excel/pchome-review` 匯出同步增加決策信封 ID、決策類型、建議代碼、責任人、資料品質、自動執行允許與證據摘要,讓線上操作與下載檔都保留同一份 guardrails。
|
||||
- V10.456 將 PChome 覆核隊列接上 `decision_envelope` contract:`fetch_competitor_review_queue()` 與 `/api/pchome-review/queue` 每筆候選都輸出同一份 SKU、PChome 候選、match evidence、recommended_action、expected_impact 與 HITL guardrails,Dashboard、Agent、Telegram、PPT 後續不得再各自重建比價判讀格式;同版將 review queue cache key 升到 v3,避免正式環境沿用舊 payload。
|
||||
|
||||
@@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.458"
|
||||
SYSTEM_VERSION = "V10.459"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **最後更新**: 2026-05-24 (台北時間)
|
||||
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 備援預設關閉
|
||||
> **適用版本**: V10.458
|
||||
> **適用版本**: V10.459
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
- 將狀態拆成:尚未搜尋、價格過期待刷新、近門檻可救回、證據不足、既有強配對保護、已排除、需單位價比較、找不到同款。
|
||||
- 每筆覆核要顯示候選 PChome 商品、候選價、match score、診斷原因、下一步動作。
|
||||
- 人工採用 / 否決 / 單位價 / 補搜尋必須能回寫 review queue,並影響 feeder 後續行為。
|
||||
- 2026-05-24 23:40 CST 起,`protected_existing_match` 的 review `decision_envelope` 會解析 `existing_match_conflict`,列出既有正式候選、新候選、雙方 score 與 delta;這類案件仍不可自動覆蓋正式價差,但人工覆核、Agent 與 PPT 不再只看到籠統「既有保護」。
|
||||
|
||||
## 3. 12 Agent 決策信封整合
|
||||
|
||||
@@ -46,6 +47,7 @@
|
||||
- 2026-05-24 23:00 CST 起,`fetch_competitor_review_queue()`、`fetch_competitor_review_queue_page()` 與 `/api/pchome-review/queue` 每筆候選也帶 `decision_envelope`,包含 SKU/PChome 標的、match evidence、人工下一步、預期價差與不可自動寫正式價差的 guardrails;Dashboard、Agent、Telegram、PPT 後續共用此 contract。
|
||||
- 2026-05-24 23:15 CST 起,Dashboard 覆核卡與 PChome 覆核 Excel 匯出也顯示/輸出信封摘要、資料品質、HITL、trace、自動執行阻擋原因與證據摘要;下載檔不得丟失 guardrails。
|
||||
- 2026-05-24 23:25 CST 起,OpenClaw 週報/日報/月報與 competitor PPT 使用 `summarize_review_decision_envelopes()` 的同一份 HITL 信封摘要,不再手寫 attempt_status 統計或自行翻譯覆核狀態。
|
||||
- 2026-05-24 23:40 CST 起,`compare_existing_identity` 成為 `protected_existing_match` 的明確建議動作;Agent 只能提示「比較既有正式候選與新候選」,不得因新候選分數較高自動寫正式價差。
|
||||
- 告警不得再輸出空泛「預期效益」;必須帶資料品質、證據來源、HITL 邊界與 trace id。
|
||||
- Agent 建議只能輔助排序與分析,不得繞過 matcher / feeder / review service 寫正式價格。
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
## 📅 詳細更新日誌 (考古存檔)
|
||||
|
||||
### 2026-05-24:PChome 近門檻身份回收第二輪
|
||||
- **V10.459 protected_existing_match 決策封包**: PChome 覆核信封開始解析 `existing_match_conflict`,把既有正式候選、新候選、雙方 matcher score 與 score delta 寫入 evidence / expected_impact / guardrails;新候選即使分數較高也維持 `can_auto_execute=false`,但 OpenClaw、PPT、Dashboard 與人工覆核可清楚看見該比較哪兩個候選。
|
||||
- **V10.458 OpenClaw / PPT 決策信封摘要**: 新增 `summarize_review_decision_envelopes()` 作為 PChome 覆核信封共用摘要 formatter;OpenClaw 週報/日報/月報、OpenClaw Bot competitor PPT data_summary 與 PPT KPI slide 都使用同一份 HITL / 資料品質 / action / trace 摘要,不再各自手寫 attempt_status 翻譯。
|
||||
- **V10.457 Dashboard / Excel 決策信封連動**: 商品看板 PChome 覆核卡顯示 `decision_envelope` 的決策等級、資料品質、HITL 與 trace;`/api/export/excel/pchome-review` 匯出新增決策信封 ID、建議代碼、責任人、資料品質、自動執行允許、阻擋原因與證據摘要,讓下載檔仍保留不可自動寫正式價差的 guardrails。
|
||||
- **V10.456 review queue 決策信封**: `fetch_competitor_review_queue()`、`fetch_competitor_review_queue_page()` 與 `/api/pchome-review/queue` 每筆 PChome 覆核候選都輸出 `decision_envelope`,包含標的 SKU/PChome 候選、match evidence、建議人工動作、預期價差、資料品質與「不可自動寫正式價差」guardrails;review queue cache key 升到 v3,避免正式環境沿用舊 payload。
|
||||
|
||||
@@ -13,6 +13,7 @@ from __future__ import annotations
|
||||
import os
|
||||
import json
|
||||
import pickle
|
||||
import re
|
||||
import time
|
||||
from datetime import date, datetime, timedelta
|
||||
from pathlib import Path
|
||||
@@ -99,6 +100,7 @@ MANUAL_REVIEW_ACTION_LABELS = {
|
||||
"needs_research": "需補搜尋",
|
||||
}
|
||||
DECISION_ACTION_LABELS = {
|
||||
"compare_existing_identity": "比較既有正式候選與新候選",
|
||||
"review_accept_identity": "人工覆核後採用同款",
|
||||
"unit_price_required": "確認單位價 / 組合差異",
|
||||
"needs_research": "補搜尋詞或重新抓取",
|
||||
@@ -318,7 +320,9 @@ def _review_action_code(attempt_status: str) -> str:
|
||||
return "needs_research"
|
||||
if attempt_status in {"identity_veto", "manual_rejected"}:
|
||||
return "verify_or_reject_identity"
|
||||
if attempt_status in {"expired_match", "protected_existing_match"}:
|
||||
if attempt_status == "protected_existing_match":
|
||||
return "compare_existing_identity"
|
||||
if attempt_status == "expired_match":
|
||||
return "refresh_or_compare_identity"
|
||||
return "human_review"
|
||||
|
||||
@@ -348,11 +352,37 @@ def _review_severity(attempt_status: str, item: dict[str, Any]) -> str:
|
||||
return "P2"
|
||||
if attempt_status in UNIT_COMPARABLE_STATUSES:
|
||||
return "P2"
|
||||
if attempt_status == "protected_existing_match":
|
||||
conflict = item.get("existing_match_conflict")
|
||||
if isinstance(conflict, dict) and _num(conflict.get("score_delta")) >= 0.03:
|
||||
return "P2"
|
||||
return "P3"
|
||||
if attempt_status in {"recoverable_low_score", "expired_match"}:
|
||||
return "P3"
|
||||
return "P4"
|
||||
|
||||
|
||||
_EXISTING_MATCH_FIELD_RE = re.compile(r"\b(existing_id|incoming_id|existing_score|incoming_score)=([^;]+)")
|
||||
|
||||
|
||||
def _parse_existing_match_conflict(error_message: Any) -> dict[str, Any]:
|
||||
text = str(error_message or "")
|
||||
if "existing_match_conflict" not in text:
|
||||
return {}
|
||||
fields = {match.group(1): match.group(2).strip() for match in _EXISTING_MATCH_FIELD_RE.finditer(text)}
|
||||
if not fields:
|
||||
return {}
|
||||
existing_score = _num(fields.get("existing_score"))
|
||||
incoming_score = _num(fields.get("incoming_score"))
|
||||
return {
|
||||
"existing_product_id": fields.get("existing_id") or "",
|
||||
"incoming_product_id": fields.get("incoming_id") or "",
|
||||
"existing_score": round(existing_score, 3),
|
||||
"incoming_score": round(incoming_score, 3),
|
||||
"score_delta": round(incoming_score - existing_score, 3),
|
||||
}
|
||||
|
||||
|
||||
def _build_review_decision_envelope(item: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Build the shared evidence contract for an operator review queue item."""
|
||||
attempt_status = str(item.get("attempt_status") or "")
|
||||
@@ -402,6 +432,22 @@ def _build_review_decision_envelope(item: dict[str, Any]) -> dict[str, Any]:
|
||||
"value": reason_text,
|
||||
"basis": "match_diagnostic_json.reasons",
|
||||
})
|
||||
existing_conflict = item.get("existing_match_conflict")
|
||||
if isinstance(existing_conflict, dict) and existing_conflict:
|
||||
score_delta = _num(existing_conflict.get("score_delta"))
|
||||
incoming_score = _num(existing_conflict.get("incoming_score"))
|
||||
existing_score = _num(existing_conflict.get("existing_score"))
|
||||
evidence.append({
|
||||
"type": "conflict",
|
||||
"metric": "existing_match_conflict",
|
||||
"value": (
|
||||
f"新候選 {existing_conflict.get('incoming_product_id') or 'unknown'} "
|
||||
f"{incoming_score:.3f} vs "
|
||||
f"既有候選 {existing_conflict.get('existing_product_id') or 'unknown'} "
|
||||
f"{existing_score:.3f}"
|
||||
),
|
||||
"basis": f"score_delta={score_delta:+.3f}; overwrite_protection=on",
|
||||
})
|
||||
|
||||
return {
|
||||
"decision_id": (
|
||||
@@ -429,6 +475,7 @@ def _build_review_decision_envelope(item: dict[str, Any]) -> dict[str, Any]:
|
||||
"expected_impact": {
|
||||
"gap_amount": gap_amount,
|
||||
"candidate_gap_pct": gap_pct,
|
||||
"existing_match_conflict": existing_conflict if isinstance(existing_conflict, dict) else {},
|
||||
"risk_reduction": "medium" if attempt_status in {"rescore_accepted_current", "recoverable_low_score"} else "watch",
|
||||
},
|
||||
"confidence": round(_num(item.get("best_match_score")), 3),
|
||||
@@ -437,6 +484,7 @@ def _build_review_decision_envelope(item: dict[str, Any]) -> dict[str, Any]:
|
||||
"blocked_reason": "PChome 候選需人工覆核;不得自動寫入正式 competitor_prices",
|
||||
"data_quality": _review_data_quality(attempt_status, item),
|
||||
"attempt_status": attempt_status,
|
||||
"existing_match_protected": bool(existing_conflict),
|
||||
"match_type": item.get("match_type") or "",
|
||||
"price_basis": item.get("price_basis") or "",
|
||||
"alert_tier": item.get("alert_tier") or "",
|
||||
@@ -556,6 +604,7 @@ def _format_competitor_review_item(row: dict[str, Any]) -> dict[str, Any]:
|
||||
alert_tier = diagnostic_payload.get("alert_tier") or _tag_suffix(tags, "alert_tier") or ""
|
||||
evidence_flags = diagnostic_payload.get("evidence_flags") or []
|
||||
diagnostic_reasons = _extract_match_diagnostic_reasons(match_diagnostic, diagnostic_payload)
|
||||
existing_match_conflict = _parse_existing_match_conflict(match_diagnostic)
|
||||
formatted = {
|
||||
"sku": str(item.get("sku") or ""),
|
||||
"name": item.get("name") or "",
|
||||
@@ -579,6 +628,7 @@ def _format_competitor_review_item(row: dict[str, Any]) -> dict[str, Any]:
|
||||
"evidence_flags": list(evidence_flags) if isinstance(evidence_flags, list) else [],
|
||||
"diagnostic_reasons": diagnostic_reasons,
|
||||
"diagnostic_reason_text": "、".join(reason["label"] for reason in diagnostic_reasons),
|
||||
"existing_match_conflict": existing_match_conflict,
|
||||
"attempted_at": _date_label(item.get("attempted_at")),
|
||||
"unit_comparison": unit_comparison,
|
||||
}
|
||||
|
||||
@@ -220,6 +220,42 @@ def test_rescore_accepted_review_item_has_actionable_decision_envelope():
|
||||
assert any(evidence["metric"] == "candidate_gap_pct" for evidence in envelope["evidence"])
|
||||
|
||||
|
||||
def test_protected_existing_match_envelope_explains_candidate_conflict():
|
||||
from services.competitor_intel_repository import _format_competitor_review_item
|
||||
|
||||
item = _format_competitor_review_item({
|
||||
"sku": "14338675",
|
||||
"name": "【Relove】胺基酸私密潔淨精華凝露120ml",
|
||||
"momo_price": 399,
|
||||
"attempt_status": "protected_existing_match",
|
||||
"candidate_count": 2,
|
||||
"best_competitor_product_id": "QEAE1O-A900A6DRS",
|
||||
"best_competitor_product_name": "RELOVE胺基酸私密清潔凝露120ml",
|
||||
"best_competitor_price": 329,
|
||||
"best_match_score": 0.817,
|
||||
"match_diagnostic_json": {
|
||||
"match_type": "exact",
|
||||
"price_basis": "total_price",
|
||||
"alert_tier": "identity_review",
|
||||
"reasons": ["strong_exact_spec_match", "spec_name_alignment"],
|
||||
},
|
||||
"error_message": (
|
||||
"existing_match_conflict;existing_id=QEAE1O-A900A6DNN;"
|
||||
"incoming_id=QEAE1O-A900A6DRS;existing_score=0.766;incoming_score=0.817"
|
||||
),
|
||||
})
|
||||
|
||||
envelope = item["decision_envelope"]
|
||||
conflict = envelope["expected_impact"]["existing_match_conflict"]
|
||||
assert item["existing_match_conflict"]["score_delta"] == 0.051
|
||||
assert envelope["recommended_action"]["action"] == "compare_existing_identity"
|
||||
assert envelope["severity"] == "P2"
|
||||
assert envelope["guardrails"]["existing_match_protected"] is True
|
||||
assert conflict["existing_product_id"] == "QEAE1O-A900A6DNN"
|
||||
assert conflict["incoming_product_id"] == "QEAE1O-A900A6DRS"
|
||||
assert any(evidence["metric"] == "existing_match_conflict" for evidence in envelope["evidence"])
|
||||
|
||||
|
||||
def test_review_decision_brief_is_shared_by_openclaw_and_ppt():
|
||||
from services.competitor_intel_repository import (
|
||||
_format_competitor_review_item,
|
||||
|
||||
Reference in New Issue
Block a user