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