From 9c7eb6f89523f03a6182df62560476872ec3bfd7 Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 10 Jul 2026 00:41:39 +0800 Subject: [PATCH] fix(ai): classify empty PixelRAG VLM offers as platform barriers --- TODO_NEXT_STEPS.txt | 2 +- config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 1 + .../pixelrag_vlm_replay_worker_service.py | 18 ++++- ...test_pixelrag_vlm_replay_worker_service.py | 67 +++++++++++++++++++ 5 files changed, 87 insertions(+), 3 deletions(-) diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 2cd9e3c..23f4408 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -9,7 +9,7 @@ python scripts/ops/check_production_version_truth.py 目前最新版本仍以 production `https://mo.wooo.work/health` readback 為準。 -本輪 source target 為 `V10.755`;部署完成前不得宣稱正式環境已是 `V10.755`。 +本輪 source target 為 `V10.756`;部署完成前不得宣稱正式環境已是 `V10.756`。 舊 iCloud checkout 不是 Gitea dev worktree,不得拿來當最新版本真相。 ================================================================================ diff --git a/config.py b/config.py index 792a9f9..a7cd203 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.755" +SYSTEM_VERSION = "V10.756" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index 87ff2bb..47c0afa 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -885,6 +885,7 @@ POSTGRES_HOST=momo-db | 2026-07-10 | PixelRAG VLM execute preflight 必須驗證 generate route | V10.753 起 `/api/ai-automation/pixelrag-vlm-route-readiness?probe_generate=true` 與 `scripts/ops/report_pixelrag_vlm_route_readiness.py --probe-generate` 可對已安裝候選模型執行極小 `/api/generate` preflight;smoke 預設仍不呼叫模型。`run_pixelrag_vlm_replay_worker.py --execute` 預設先執行 generate preflight,若 GCP-A direct / 110 proxy timeout、GCP-B candidate 雖安裝但 generate 不健康,worker 必須在送入 screenshot tiles 前寫 `model_route_not_ready` artifact receipt,輸出 `tag_model_route_ready`、`generate_route_ready`、`route_model_call_performed` 與 `tile_model_call_performed=false`,下一步固定為 `repair_ollama_vlm_generate_runtime_or_proxy_timeout`;不得把 tags 可見誤當 VLM 可執行。 | | 2026-07-10 | PixelRAG VLM tile generate 必須綁定 preflight 選出的 host / exact model | V10.754 起 `run_pixelrag_vlm_replay_worker.py --execute` 若 route readiness 已輸出 `candidate_host`,tile VLM generate 必須使用該 approved host 與 exact `candidate_model` 直呼 `/api/generate`,不得重新進入全域 Ollama resolver、不得因 111 fallback 規則把 VLM 模型降級成文字模型、不得再出現 preflight 選 111 但 tile generate tried GCP-B/GCP-A/111 的 route drift。receipt 必須輸出 `route_candidate_host`,模型錯誤時下一步為 `repair_ollama_vlm_generate_runtime_or_proxy_timeout`,仍然不寫 DB、不寫正式價格表。 | | 2026-07-10 | PixelRAG VLM no-probe 監控不得把 unknown 顯示成 failed | V10.755 起 smoke / scheduled-health 的 `pixelrag_vlm_route_readiness.details.generate_route_ready` 在 `generate_probe_performed=false` 時必須保留為 `null`,表示尚未做 `/api/generate` preflight;不得把未探測狀態轉成 `false`,避免 UI 或監控把 tags-only readiness 誤讀為 generate failure。 | +| 2026-07-10 | PixelRAG VLM 空欄位與 Shopee language interstitial 必須自動轉 platform probe | V10.756 起 VLM prompt 明確把 language / region / app-download / landing / loading / logo-only / cookie consent pages 視為非商品卡;若 VLM 回傳所有 offer fields 皆空且 required title / price 缺失,worker 會標記 `non_product_or_interstitial_detected=true`,receipt 下一步改為 `run_platform_probe_or_use_structured_api`,不得反覆要求人工審核或無限 rerun VLM。 | | 2026-06-29 | PChome DB apply 授權 lane 必須先通過 no-write guard / decision preflight / decision closeout / issuer gate / signing-decision preflight / signing-decision closeout / signing-issuer guard | V10.725 的 PChome mapping backlog auto-policy 已新增 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-lane-guard`、`/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-decision-preflight`、`/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-decision-closeout`、`/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-issuer-gate`、`/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-decision-preflight`、`/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-decision-closeout` 與 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-issuer-guard`;這些 endpoint 只驗證 final exact request package、same-run production truth requirement、secret rejection、rollback boundary、lane entry requirements、decision input requirements、rejection policy、post-apply verifier、future authorization decision package、final nonsecret authorization envelope、signing decision preflight inputs、unsigned signing decision package 與 signable request boundary,不讀 secret、不執行 shell/SQL、不寫 DB,也不簽發 database apply authorization。 | | 2026-06-29 | PChome DB apply 授權簽署發行者 lane 必須先產出 final signable request package | V10.725 的 PChome mapping backlog auto-policy 新增 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-issuer-closeout`;此 endpoint 只把 signing-issuer guard 的 signable request boundary 收斂成 final signable request package 與 closeout contract,確認 fresh production truth、post-apply verifier、migration hash、secret boundary 與 no-side-effect checks,不讀 secret、不簽發 authorization、不執行 shell/SQL、不寫 DB,也不代表正式 DB apply 已授權。 | | 2026-06-29 | PChome DB apply 授權簽署執行 lane 必須先通過 operator-held secret boundary preflight | V10.725 的 PChome mapping backlog auto-policy 新增 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-execution-preflight`;此 endpoint 只把 final signable request package 轉成 future signing execution preflight package、operator-held secret boundary contract、nonsecret signing inputs、command-shape preview、rollback boundary 與 abort conditions,不讀 secret、不接受 plaintext secret、不簽發 authorization、不執行 shell/SQL、不寫 DB,也不代表正式 DB apply 已授權。 | diff --git a/services/pixelrag_vlm_replay_worker_service.py b/services/pixelrag_vlm_replay_worker_service.py index d8d2306..fceb7b3 100644 --- a/services/pixelrag_vlm_replay_worker_service.py +++ b/services/pixelrag_vlm_replay_worker_service.py @@ -153,6 +153,8 @@ def _prompt_for_item(item: Mapping[str, Any]) -> str: "Use only visible tile evidence and cite evidence_refs like tile:1.\n" "If the tile is access denied, captcha, login, traffic verification, or not a product/search card, " "set blocked_page_detected=true and leave product fields empty.\n" + "Language selection, region selection, app-download, landing, loading, logo-only, or cookie consent " + "pages are not product/search cards; set blocked_page_detected=true for them.\n" "Required JSON schema:\n" "{\n" ' "blocked_page_detected": false,\n' @@ -183,6 +185,7 @@ def _validate_model_payload( missing_required: list[str] = [] field_evidence_missing: list[str] = [] low_confidence_fields: list[str] = [] + present_field_count = 0 blocked_detected = bool(parsed.get("blocked_page_detected")) for contract in list(item.get("field_contract") or []): @@ -198,6 +201,8 @@ def _validate_model_payload( confidence = 0.0 min_confidence = float(contract.get("min_confidence") or DEFAULT_CONFIDENCE_THRESHOLD) present = _field_value_present(value) + if present: + present_field_count += 1 if present and not evidence_refs: field_evidence_missing.append(field_name) if present and confidence < min_confidence: @@ -213,14 +218,22 @@ def _validate_model_payload( for field in declared_missing: if field not in missing_required: missing_required.append(field) + non_product_or_interstitial_detected = ( + not blocked_detected + and present_field_count == 0 + and bool(missing_required) + ) return { "blocked_page_detected": blocked_detected, + "non_product_or_interstitial_detected": non_product_or_interstitial_detected, + "present_field_count": present_field_count, "missing_required_fields": missing_required, "field_evidence_missing": field_evidence_missing, "low_confidence_fields": low_confidence_fields, "valid_for_identity_matcher_replay": ( not blocked_detected + and not non_product_or_interstitial_detected and not missing_required and not field_evidence_missing ), @@ -501,9 +514,12 @@ def _execute_item( missing_required = list(validation.get("missing_required_fields") or []) evidence_missing = list(validation.get("field_evidence_missing") or []) blocked_detected = bool(validation.get("blocked_page_detected")) + non_product_or_interstitial = bool( + validation.get("non_product_or_interstitial_detected") + ) status = "executed_ok" next_action = "run_identity_matcher_replay_then_promotion_gate" - if blocked_detected: + if blocked_detected or non_product_or_interstitial: status = "executed_warning" next_action = "run_platform_probe_or_use_structured_api" elif missing_required or evidence_missing: diff --git a/tests/test_pixelrag_vlm_replay_worker_service.py b/tests/test_pixelrag_vlm_replay_worker_service.py index b0179bc..64a2abc 100644 --- a/tests/test_pixelrag_vlm_replay_worker_service.py +++ b/tests/test_pixelrag_vlm_replay_worker_service.py @@ -177,6 +177,73 @@ def test_pixelrag_vlm_replay_worker_writes_model_error_receipt(tmp_path, monkeyp assert receipt["next_machine_action"] == "repair_ollama_vlm_runtime_or_model_route" +def test_pixelrag_vlm_replay_worker_empty_fields_routes_to_platform_probe(tmp_path, monkeypatch): + from services import pixelrag_vlm_replay_worker_service as service + + _write_receipt( + tmp_path, + platform="shopee_tw", + manifest_id="shopee-language", + title="蝦皮購物 | 花得更少買得更好", + url="https://shopee.tw/search?keyword=sunscreen", + ) + + class FakeOllama: + def __init__(self, model): + self.model = model + + def generate(self, *args, **kwargs): + return SimpleNamespace( + success=True, + content=json.dumps({ + "blocked_page_detected": False, + "fields": { + "title": {"value": None, "confidence": 0.0, "evidence_refs": []}, + "price": {"value": None, "confidence": 0.0, "evidence_refs": []}, + "currency": {"value": None, "confidence": 0.0, "evidence_refs": []}, + }, + "quality": { + "overall_confidence": 0.0, + "missing_required_fields": [], + "requires_identity_matcher_replay": True, + "requires_promotion_gate": True, + }, + "notes": [], + }), + model="minicpm-v:latest", + error=None, + total_duration=2.0, + host="http://192.168.0.111:11434", + input_tokens=10, + output_tokens=40, + ) + + monkeypatch.setattr(service, "OllamaService", FakeOllama) + payload = service.run_pixelrag_ollama_vlm_replay_worker( + artifact_root=tmp_path, + output_root=tmp_path / "receipts", + platform="shopee_tw", + execute=True, + write_receipt=True, + tile_limit=4, + auto_select_model=False, + ) + + assert payload["status"] == "warning" + assert payload["summary"]["executed_warning_count"] == 1 + assert payload["summary"]["required_field_missing_count"] == 2 + assert payload["next_machine_action"] == "rerun_vlm_replay_with_more_tiles_or_platform_probe" + item = payload["worker_items"][0] + assert item["worker_status"] == "executed_warning" + assert item["next_machine_action"] == "run_platform_probe_or_use_structured_api" + assert item["validation"]["present_field_count"] == 0 + assert item["validation"]["non_product_or_interstitial_detected"] is True + assert item["validation"]["valid_for_identity_matcher_replay"] is False + receipt_path = tmp_path / "receipts" / "shopee_tw" / "shopee-language" / "vlm_replay_receipt.json" + receipt = json.loads(receipt_path.read_text(encoding="utf-8")) + assert receipt["next_machine_action"] == "run_platform_probe_or_use_structured_api" + + def test_pixelrag_vlm_replay_worker_auto_selects_installed_candidate(tmp_path, monkeypatch): from services import pixelrag_vlm_replay_worker_service as service