Files
ewoooc/services/pixelrag_ocr_vlm_replay_service.py

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",
]