355 lines
14 KiB
Python
355 lines
14 KiB
Python
"""Read-only PixelRAG OCR/VLM replay contract builder.
|
|
|
|
This module turns PixelRAG visual evidence receipts into a machine-readable
|
|
contract for an Ollama-first OCR/VLM worker. It does not execute OCR, call a
|
|
model, write RAG data, write price tables, or promote visual fields.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any, Mapping
|
|
|
|
from services.pixelrag_crawler_integration_service import (
|
|
DEFAULT_ARTIFACT_MAX_AGE_HOURS,
|
|
DEFAULT_ARTIFACT_ROOT,
|
|
ECOMMERCE_PLATFORM_PROFILES,
|
|
)
|
|
from services.pixelrag_rag_candidate_replay_service import (
|
|
build_pixelrag_rag_candidate_replay_readback,
|
|
)
|
|
|
|
|
|
POLICY = "read_only_pixelrag_ocr_vlm_replay_contract_v1"
|
|
DEFAULT_LIMIT = 50
|
|
DEFAULT_CONFIDENCE_THRESHOLD = 0.72
|
|
|
|
FIELD_ALIASES: dict[str, dict[str, str]] = {
|
|
"title": {"type": "string", "required": "true", "evidence": "tile_text_or_card_region"},
|
|
"price": {"type": "money", "required": "true", "evidence": "price_region"},
|
|
"currency": {"type": "string", "required": "false", "evidence": "price_region"},
|
|
"bundle_spec": {"type": "string", "required": "false", "evidence": "spec_or_bundle_region"},
|
|
"promo_badge": {"type": "string", "required": "false", "evidence": "badge_region"},
|
|
"campaign_badge": {"type": "string", "required": "false", "evidence": "badge_region"},
|
|
"stock_hint": {"type": "string", "required": "false", "evidence": "availability_region"},
|
|
"image": {"type": "asset_ref", "required": "false", "evidence": "screenshot_or_tile"},
|
|
"product_id": {"type": "string", "required": "false", "evidence": "url_or_card_region"},
|
|
"shop": {"type": "string", "required": "false", "evidence": "seller_region"},
|
|
"sold_count": {"type": "number_or_label", "required": "false", "evidence": "social_proof_region"},
|
|
"rating": {"type": "number_or_label", "required": "false", "evidence": "rating_region"},
|
|
"shipping_badge": {"type": "string", "required": "false", "evidence": "shipping_region"},
|
|
"discount": {"type": "string", "required": "false", "evidence": "discount_region"},
|
|
"delivery_badge": {"type": "string", "required": "false", "evidence": "delivery_region"},
|
|
"spec": {"type": "string", "required": "false", "evidence": "spec_region"},
|
|
"review_count": {"type": "number_or_label", "required": "false", "evidence": "review_region"},
|
|
"coupon": {"type": "string", "required": "false", "evidence": "coupon_region"},
|
|
"sales_hint": {"type": "string", "required": "false", "evidence": "social_proof_region"},
|
|
"member_price": {"type": "money", "required": "false", "evidence": "member_price_region"},
|
|
"shipping_hint": {"type": "string", "required": "false", "evidence": "shipping_region"},
|
|
"reward_badge": {"type": "string", "required": "false", "evidence": "reward_region"},
|
|
"point_reward": {"type": "string", "required": "false", "evidence": "reward_region"},
|
|
"public_page_visual_evidence": {
|
|
"type": "evidence_summary",
|
|
"required": "true",
|
|
"evidence": "visible_page_regions",
|
|
},
|
|
}
|
|
|
|
|
|
def _normalise_platforms(
|
|
platform: str | tuple[str, ...] | list[str] | None,
|
|
) -> tuple[str, ...] | None:
|
|
if isinstance(platform, str):
|
|
return (platform.strip().lower(),) if platform.strip() else None
|
|
selected = tuple(
|
|
str(item or "").strip().lower()
|
|
for item in (platform or [])
|
|
if str(item or "").strip()
|
|
)
|
|
return selected or None
|
|
|
|
|
|
def _tile_inputs(candidate: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
files = ((candidate.get("file_summary") or {}).get("files") or [])
|
|
tiles: list[dict[str, Any]] = []
|
|
for index, item in enumerate(files):
|
|
if item.get("kind") != "tile":
|
|
continue
|
|
path = str(item.get("path") or "").strip()
|
|
tiles.append({
|
|
"index": index,
|
|
"kind": "tile",
|
|
"path": path,
|
|
"exists": bool(item.get("exists")),
|
|
"evidence_ref": f"tile:{index}",
|
|
})
|
|
return tiles
|
|
|
|
|
|
def _field_contract(platform: str) -> list[dict[str, Any]]:
|
|
profile = ECOMMERCE_PLATFORM_PROFILES.get(platform) or {}
|
|
fields = [str(item) for item in list(profile.get("data_targets") or []) if str(item)]
|
|
if "price" in fields and "currency" not in fields:
|
|
fields.insert(fields.index("price") + 1, "currency")
|
|
if not fields:
|
|
fields = ["public_page_visual_evidence"]
|
|
contracts: list[dict[str, Any]] = []
|
|
for field in fields:
|
|
base = FIELD_ALIASES.get(field) or {
|
|
"type": "string",
|
|
"required": "false",
|
|
"evidence": "visible_region",
|
|
}
|
|
contracts.append({
|
|
"field": field,
|
|
"type": base["type"],
|
|
"required": base["required"] == "true",
|
|
"evidence_requirement": base["evidence"],
|
|
"min_confidence": DEFAULT_CONFIDENCE_THRESHOLD if field in {"title", "price"} else 0.62,
|
|
"allow_empty": base["required"] != "true",
|
|
})
|
|
return contracts
|
|
|
|
|
|
def _output_schema(fields: list[dict[str, Any]]) -> dict[str, Any]:
|
|
return {
|
|
"platform": "string",
|
|
"manifest_id": "string",
|
|
"source_receipt_path": "string",
|
|
"blocked_page_detected": "boolean",
|
|
"quality": {
|
|
"overall_confidence": "float_0_1",
|
|
"field_confidence_min": "float_0_1",
|
|
"missing_required_fields": "list[string]",
|
|
"requires_identity_matcher_replay": "boolean",
|
|
"requires_promotion_gate": "boolean",
|
|
},
|
|
"fields": {
|
|
item["field"]: {
|
|
"value": item["type"],
|
|
"confidence": "float_0_1",
|
|
"evidence_refs": "list[string]",
|
|
}
|
|
for item in fields
|
|
},
|
|
"next_machine_action": "string",
|
|
}
|
|
|
|
|
|
def _validation_rules() -> list[str]:
|
|
return [
|
|
"blocked_or_access_denied_page_must_not_emit_product_fields",
|
|
"every_field_value_requires_at_least_one_tile_evidence_ref",
|
|
"required_title_and_price_must_meet_min_confidence_before_promotion_gate",
|
|
"price_must_parse_to_numeric_money_before_any_price_candidate",
|
|
"currency_defaults_to_TWD_only_when_page_locale_or_symbol_supports_it",
|
|
"identity_matcher_replay_required_before_offer_matching",
|
|
"promotion_gate_required_before_internal_rag_or_ai_insights_write",
|
|
"direct_competitor_prices_write_forbidden",
|
|
"direct_competitor_price_history_write_forbidden",
|
|
"direct_ai_insights_write_forbidden",
|
|
]
|
|
|
|
|
|
def _model_route_contract() -> dict[str, Any]:
|
|
return {
|
|
"runtime_policy": "ollama_first_no_hosted_vlm_default",
|
|
"preferred_chain": [
|
|
{"host": "34.87.90.216:11434", "role": "gcp_a_primary"},
|
|
{"host": "34.21.145.224:11434", "role": "gcp_b_secondary"},
|
|
{"host": "192.168.0.111:11434", "role": "lan_final_fallback"},
|
|
],
|
|
"production_app_host_is_not_ollama_node": "192.168.0.188",
|
|
"hosted_vlm_api_allowed": False,
|
|
"gemini_default_allowed": False,
|
|
"secret_read_allowed": False,
|
|
"network_call_performed_by_this_contract": False,
|
|
}
|
|
|
|
|
|
def _candidate_contract(candidate: Mapping[str, Any]) -> dict[str, Any]:
|
|
platform = str(candidate.get("platform") or "unknown")
|
|
fields = _field_contract(platform)
|
|
tiles = _tile_inputs(candidate)
|
|
blocked_reasons = list(candidate.get("blocked_reasons") or [])
|
|
candidate_status = str(candidate.get("candidate_status") or "unknown")
|
|
ready = candidate_status == "eligible" and bool(tiles)
|
|
missing_tile_count = sum(1 for item in tiles if not item["exists"])
|
|
if candidate_status == "eligible" and not tiles:
|
|
blocked_reasons.append("no_tile_inputs_available")
|
|
if missing_tile_count:
|
|
blocked_reasons.append("tile_input_missing_files")
|
|
if candidate_status == "invalid":
|
|
replay_status = "invalid"
|
|
elif ready and not missing_tile_count:
|
|
replay_status = "ready_for_ocr_vlm_replay"
|
|
else:
|
|
replay_status = "blocked"
|
|
|
|
return {
|
|
"replay_status": replay_status,
|
|
"ready_for_ollama_vlm_worker": replay_status == "ready_for_ocr_vlm_replay",
|
|
"candidate_status": candidate_status,
|
|
"blocked_reasons": blocked_reasons,
|
|
"platform": platform,
|
|
"manifest_id": candidate.get("manifest_id"),
|
|
"source_receipt_path": candidate.get("receipt_path"),
|
|
"url": candidate.get("url"),
|
|
"title_hint": candidate.get("title"),
|
|
"http_status": candidate.get("http_status"),
|
|
"visual_barrier_detected": bool(candidate.get("visual_barrier_detected")),
|
|
"visual_barrier_reason": candidate.get("visual_barrier_reason"),
|
|
"input_tiles": tiles,
|
|
"field_contract": fields,
|
|
"field_contract_count": len(fields),
|
|
"output_schema": _output_schema(fields),
|
|
"validation_rules": _validation_rules(),
|
|
"prompt_contract": {
|
|
"task": "extract_public_marketplace_offer_card_fields_from_tiles",
|
|
"language": "zh-TW",
|
|
"allowed_inputs": ["tile images", "receipt metadata", "field contract"],
|
|
"forbidden_inputs": ["secrets", "cookies", "sessions", "private account pages"],
|
|
"instruction": (
|
|
"Return only fields supported by visible tile evidence; mark unknown "
|
|
"instead of guessing, and set blocked_page_detected=true for access "
|
|
"denied, captcha, login, or traffic verification pages."
|
|
),
|
|
},
|
|
"automation_boundary": {
|
|
"ocr_execution_performed": False,
|
|
"vlm_execution_performed": False,
|
|
"writes_database": False,
|
|
"writes_ai_insights": False,
|
|
"writes_price_tables": False,
|
|
"network_call": False,
|
|
"secret_read": False,
|
|
},
|
|
"next_machine_action": (
|
|
"run_ollama_first_vlm_replay_worker"
|
|
if replay_status == "ready_for_ocr_vlm_replay"
|
|
else (
|
|
"run_platform_probe_or_use_structured_api"
|
|
if candidate.get("visual_barrier_detected") or candidate.get("http_status", 0) >= 400
|
|
else "refresh_pixelrag_visual_capture_receipt"
|
|
)
|
|
),
|
|
}
|
|
|
|
|
|
def build_pixelrag_ocr_vlm_replay_contract(
|
|
*,
|
|
artifact_root: str | Path | None = None,
|
|
platform: str | tuple[str, ...] | list[str] | None = None,
|
|
max_age_hours: int | None = None,
|
|
limit: int | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Build a no-write OCR/VLM replay contract from PixelRAG receipts."""
|
|
root = Path(artifact_root or DEFAULT_ARTIFACT_ROOT)
|
|
max_age = max(1, int(max_age_hours or DEFAULT_ARTIFACT_MAX_AGE_HOURS))
|
|
item_limit = max(1, min(int(limit or DEFAULT_LIMIT), 250))
|
|
platforms = _normalise_platforms(platform)
|
|
generated_at = datetime.now(timezone.utc).isoformat()
|
|
|
|
candidate_readback = build_pixelrag_rag_candidate_replay_readback(
|
|
artifact_root=root,
|
|
platform=platforms,
|
|
max_age_hours=max_age,
|
|
limit=item_limit,
|
|
)
|
|
replay_items = [
|
|
_candidate_contract(candidate)
|
|
for candidate in list(candidate_readback.get("candidates") or [])
|
|
]
|
|
ready_count = sum(1 for item in replay_items if item["replay_status"] == "ready_for_ocr_vlm_replay")
|
|
blocked_count = sum(1 for item in replay_items if item["replay_status"] == "blocked")
|
|
invalid_count = sum(1 for item in replay_items if item["replay_status"] == "invalid")
|
|
tile_input_count = sum(len(item.get("input_tiles") or []) for item in replay_items)
|
|
field_contract_count = sum(int(item.get("field_contract_count") or 0) for item in replay_items)
|
|
missing_tile_count = sum(
|
|
1
|
|
for item in replay_items
|
|
for tile in list(item.get("input_tiles") or [])
|
|
if not tile.get("exists")
|
|
)
|
|
|
|
if invalid_count and invalid_count == len(replay_items):
|
|
status = "critical"
|
|
elif not replay_items or ready_count == 0 or blocked_count or invalid_count or missing_tile_count:
|
|
status = "warning"
|
|
else:
|
|
status = "ok"
|
|
|
|
summary = {
|
|
"receipt_count": len(replay_items),
|
|
"replay_ready_count": ready_count,
|
|
"blocked_count": blocked_count,
|
|
"invalid_count": invalid_count,
|
|
"tile_input_count": tile_input_count,
|
|
"missing_tile_count": missing_tile_count,
|
|
"field_contract_count": field_contract_count,
|
|
"extraction_execution_performed": False,
|
|
"ocr_execution_performed": False,
|
|
"vlm_execution_performed": False,
|
|
"platforms": sorted({str(item.get("platform") or "unknown") for item in replay_items}),
|
|
}
|
|
|
|
return {
|
|
"success": invalid_count == 0,
|
|
"policy": POLICY,
|
|
"status": status,
|
|
"generated_at": generated_at,
|
|
"artifact_root": str(root),
|
|
"max_age_hours": max_age,
|
|
"platform_filter": list(platforms or []),
|
|
"summary": summary,
|
|
"replay_items": replay_items,
|
|
"source_candidate_replay": {
|
|
"policy": candidate_readback.get("policy"),
|
|
"status": candidate_readback.get("status"),
|
|
"summary": candidate_readback.get("summary"),
|
|
},
|
|
"model_route_contract": _model_route_contract(),
|
|
"replay_contract": {
|
|
"automation_mode": "no_write_ocr_vlm_replay_package",
|
|
"writes_database": False,
|
|
"writes_ai_insights": False,
|
|
"writes_price_tables": False,
|
|
"network_call": False,
|
|
"secret_read": False,
|
|
"extraction_execution_performed": False,
|
|
"blocked_pages_are_not_product_data": True,
|
|
"requires_identity_matcher_replay": True,
|
|
"requires_promotion_gate": True,
|
|
"requires_embedding_signature_guard": True,
|
|
},
|
|
"next_machine_action": (
|
|
"run_pixelrag_visual_capture_worker"
|
|
if not replay_items
|
|
else (
|
|
"fix_invalid_pixelrag_receipts"
|
|
if invalid_count and not ready_count
|
|
else (
|
|
"run_ollama_first_vlm_replay_worker"
|
|
if ready_count
|
|
else "run_platform_probe_or_use_structured_api"
|
|
)
|
|
)
|
|
),
|
|
"controlled_apply": {
|
|
"network_call": False,
|
|
"db_write": False,
|
|
"secret_read": False,
|
|
"production_price_write": False,
|
|
"artifact_write": False,
|
|
"primary_human_gate_count": 0,
|
|
},
|
|
}
|
|
|
|
|
|
__all__ = [
|
|
"DEFAULT_CONFIDENCE_THRESHOLD",
|
|
"POLICY",
|
|
"build_pixelrag_ocr_vlm_replay_contract",
|
|
]
|