339 lines
12 KiB
Python
339 lines
12 KiB
Python
"""Read-only PixelRAG receipt to internal RAG candidate replay bridge.
|
|
|
|
The bridge only converts saved visual evidence receipts into machine-readable
|
|
candidate payloads. It does not OCR, call a model, write ai_insights, or promote
|
|
anything into the production price tables.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
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,
|
|
_parse_iso_datetime,
|
|
_portable_receipt_file_path,
|
|
_receipt_candidates,
|
|
_safe_int,
|
|
_visual_barrier_reason,
|
|
)
|
|
|
|
|
|
POLICY = "read_only_pixelrag_rag_candidate_replay_v1"
|
|
INTERNAL_RAG_TARGET = "rag_service.learning_episode_candidate_preview"
|
|
DEFAULT_LIMIT = 50
|
|
|
|
|
|
def _status_counts(items: list[dict[str, Any]]) -> dict[str, int]:
|
|
counts: dict[str, int] = {}
|
|
for item in items:
|
|
status = str(item.get("candidate_status") or "unknown")
|
|
counts[status] = counts.get(status, 0) + 1
|
|
return counts
|
|
|
|
|
|
def _platform_from_receipt(receipt_path: Path, receipt: Mapping[str, Any]) -> str:
|
|
capture_target = receipt.get("capture_target") or {}
|
|
platform = str(capture_target.get("platform") or "").strip().lower()
|
|
if platform:
|
|
return platform
|
|
try:
|
|
return receipt_path.parent.parent.name
|
|
except IndexError:
|
|
return "unknown"
|
|
|
|
|
|
def _manifest_id_from_receipt(receipt_path: Path, receipt: Mapping[str, Any]) -> str:
|
|
return str(receipt.get("manifest_id") or receipt_path.parent.name or "").strip()
|
|
|
|
|
|
def _count_receipt_files(receipt_path: Path, receipt: Mapping[str, Any]) -> dict[str, Any]:
|
|
tile_file_count = 0
|
|
missing_file_count = 0
|
|
file_statuses: list[dict[str, Any]] = []
|
|
for item in list(receipt.get("files") or []):
|
|
kind = item.get("kind")
|
|
path = _portable_receipt_file_path(receipt_path, item.get("path"), kind)
|
|
exists = path.exists()
|
|
if kind == "tile":
|
|
tile_file_count += 1
|
|
if not exists:
|
|
missing_file_count += 1
|
|
file_statuses.append({
|
|
"kind": kind,
|
|
"path": str(path),
|
|
"exists": exists,
|
|
})
|
|
return {
|
|
"tile_file_count": tile_file_count,
|
|
"missing_file_count": missing_file_count,
|
|
"files": file_statuses,
|
|
}
|
|
|
|
|
|
def _candidate_text(
|
|
*,
|
|
platform: str,
|
|
manifest_id: str,
|
|
receipt: Mapping[str, Any],
|
|
file_summary: Mapping[str, Any],
|
|
barrier_reason: str,
|
|
) -> str:
|
|
capture_target = receipt.get("capture_target") or {}
|
|
page_metrics = receipt.get("page_metrics") or {}
|
|
profile = ECOMMERCE_PLATFORM_PROFILES.get(platform) or {}
|
|
url = str(capture_target.get("url") or page_metrics.get("final_url") or "").strip()
|
|
title = str(page_metrics.get("title") or "").strip()
|
|
data_targets = ", ".join(str(item) for item in list(profile.get("data_targets") or [])[:8])
|
|
parts = [
|
|
f"platform={platform}",
|
|
f"manifest_id={manifest_id}",
|
|
f"title={title or 'unknown'}",
|
|
f"url={url or 'unknown'}",
|
|
f"http_status={_safe_int(receipt.get('http_status'))}",
|
|
f"capture_status={receipt.get('status') or 'unknown'}",
|
|
f"tile_file_count={int(file_summary.get('tile_file_count') or 0)}",
|
|
f"missing_file_count={int(file_summary.get('missing_file_count') or 0)}",
|
|
]
|
|
if data_targets:
|
|
parts.append(f"intended_fields={data_targets}")
|
|
if barrier_reason:
|
|
parts.append(f"visual_barrier={barrier_reason}")
|
|
return " | ".join(parts)
|
|
|
|
|
|
def _candidate_from_receipt_path(
|
|
receipt_path: Path,
|
|
*,
|
|
now: datetime,
|
|
max_age_hours: int,
|
|
) -> dict[str, Any]:
|
|
errors: list[str] = []
|
|
try:
|
|
receipt = json.loads(receipt_path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError) as exc:
|
|
receipt = {}
|
|
errors.append(str(exc)[:300])
|
|
|
|
platform = _platform_from_receipt(receipt_path, receipt)
|
|
manifest_id = _manifest_id_from_receipt(receipt_path, receipt)
|
|
generated_at = _parse_iso_datetime(receipt.get("generated_at"))
|
|
age_hours = ((now - generated_at).total_seconds() / 3600) if generated_at else None
|
|
stale = age_hours is None or age_hours > max_age_hours
|
|
file_summary = _count_receipt_files(receipt_path, receipt)
|
|
missing_file_count = int(file_summary.get("missing_file_count") or 0)
|
|
barrier_reason = _visual_barrier_reason(receipt)
|
|
capture_status = str(receipt.get("status") or "").strip().lower()
|
|
http_status = _safe_int(receipt.get("http_status"))
|
|
|
|
blocked_reasons: list[str] = []
|
|
if errors:
|
|
blocked_reasons.append("receipt_parse_error")
|
|
if capture_status != "captured":
|
|
blocked_reasons.append("receipt_not_captured")
|
|
if stale:
|
|
blocked_reasons.append("receipt_stale_or_missing_timestamp")
|
|
if missing_file_count:
|
|
blocked_reasons.append("receipt_missing_files")
|
|
if barrier_reason:
|
|
blocked_reasons.append(f"visual_barrier:{barrier_reason}")
|
|
if http_status >= 400 and not barrier_reason:
|
|
blocked_reasons.append(f"http_status_{http_status}")
|
|
|
|
eligible = not blocked_reasons
|
|
candidate_status = "eligible" if eligible else ("invalid" if errors else "blocked")
|
|
capture_target = receipt.get("capture_target") or {}
|
|
page_metrics = receipt.get("page_metrics") or {}
|
|
|
|
return {
|
|
"candidate_status": candidate_status,
|
|
"eligible_for_rag_candidate_replay": eligible,
|
|
"blocked_reasons": blocked_reasons,
|
|
"platform": platform,
|
|
"manifest_id": manifest_id,
|
|
"receipt_path": str(receipt_path),
|
|
"generated_at": receipt.get("generated_at"),
|
|
"age_hours": round(age_hours, 3) if age_hours is not None else None,
|
|
"stale": stale,
|
|
"http_status": http_status,
|
|
"capture_status": receipt.get("status"),
|
|
"url": capture_target.get("url") or page_metrics.get("final_url"),
|
|
"title": page_metrics.get("title"),
|
|
"visual_barrier_detected": bool(barrier_reason),
|
|
"visual_barrier_reason": barrier_reason,
|
|
"tile_file_count": int(file_summary.get("tile_file_count") or 0),
|
|
"missing_file_count": missing_file_count,
|
|
"rag_candidate_text": _candidate_text(
|
|
platform=platform,
|
|
manifest_id=manifest_id,
|
|
receipt=receipt,
|
|
file_summary=file_summary,
|
|
barrier_reason=barrier_reason,
|
|
),
|
|
"internal_rag_target": INTERNAL_RAG_TARGET,
|
|
"promotion_boundary": {
|
|
"direct_ai_insights_write_allowed": False,
|
|
"direct_price_write_allowed": False,
|
|
"requires_ocr_or_vlm_replay": True,
|
|
"requires_identity_matcher_replay": True,
|
|
"requires_promotion_gate": True,
|
|
"requires_embedding_signature_guard": True,
|
|
},
|
|
"next_machine_action": (
|
|
"run_ocr_vlm_replay_then_promotion_gate"
|
|
if eligible
|
|
else (
|
|
"run_platform_probe_or_use_structured_api"
|
|
if barrier_reason
|
|
else "refresh_pixelrag_visual_capture_receipt"
|
|
)
|
|
),
|
|
"file_summary": file_summary,
|
|
}
|
|
|
|
|
|
def _unique_receipt_candidates(
|
|
root: Path,
|
|
*,
|
|
platforms: tuple[str, ...] | list[str] | None,
|
|
limit: int,
|
|
) -> list[Path]:
|
|
seen: set[Path] = set()
|
|
candidates: list[Path] = []
|
|
selected_platforms = [
|
|
str(platform or "").strip().lower()
|
|
for platform in (platforms or [])
|
|
if str(platform or "").strip()
|
|
]
|
|
if selected_platforms:
|
|
for platform in selected_platforms:
|
|
for path in _receipt_candidates(root, platform=platform):
|
|
resolved = path.resolve()
|
|
if resolved not in seen:
|
|
seen.add(resolved)
|
|
candidates.append(path)
|
|
else:
|
|
for path in _receipt_candidates(root):
|
|
resolved = path.resolve()
|
|
if resolved not in seen:
|
|
seen.add(resolved)
|
|
candidates.append(path)
|
|
return sorted(candidates, key=lambda path: path.stat().st_mtime, reverse=True)[:limit]
|
|
|
|
|
|
def build_pixelrag_rag_candidate_replay_readback(
|
|
*,
|
|
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 replayable RAG candidate readback 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))
|
|
if isinstance(platform, str):
|
|
platforms = (platform.strip().lower(),) if platform.strip() else None
|
|
else:
|
|
platforms = tuple(
|
|
str(item or "").strip().lower()
|
|
for item in (platform or [])
|
|
if str(item or "").strip()
|
|
) or None
|
|
now = datetime.now(timezone.utc)
|
|
|
|
candidates = _unique_receipt_candidates(root, platforms=platforms, limit=item_limit)
|
|
replay_candidates = [
|
|
_candidate_from_receipt_path(path, now=now, max_age_hours=max_age)
|
|
for path in candidates
|
|
]
|
|
eligible_count = sum(1 for item in replay_candidates if item["candidate_status"] == "eligible")
|
|
blocked_count = sum(1 for item in replay_candidates if item["candidate_status"] == "blocked")
|
|
invalid_count = sum(1 for item in replay_candidates if item["candidate_status"] == "invalid")
|
|
visual_barrier_count = sum(1 for item in replay_candidates if item["visual_barrier_detected"])
|
|
missing_file_count = sum(int(item.get("missing_file_count") or 0) for item in replay_candidates)
|
|
tile_file_count = sum(int(item.get("tile_file_count") or 0) for item in replay_candidates)
|
|
stale_count = sum(1 for item in replay_candidates if item.get("stale"))
|
|
|
|
if invalid_count and invalid_count == len(replay_candidates):
|
|
status = "critical"
|
|
elif eligible_count == 0 or blocked_count or invalid_count or visual_barrier_count:
|
|
status = "warning"
|
|
else:
|
|
status = "ok"
|
|
|
|
if not replay_candidates:
|
|
status = "warning"
|
|
|
|
summary = {
|
|
"receipt_count": len(replay_candidates),
|
|
"eligible_count": eligible_count,
|
|
"blocked_count": blocked_count,
|
|
"invalid_count": invalid_count,
|
|
"visual_barrier_count": visual_barrier_count,
|
|
"tile_file_count": tile_file_count,
|
|
"missing_file_count": missing_file_count,
|
|
"stale_count": stale_count,
|
|
"status_counts": _status_counts(replay_candidates),
|
|
"platforms": sorted({str(item.get("platform") or "unknown") for item in replay_candidates}),
|
|
}
|
|
|
|
return {
|
|
"success": invalid_count == 0,
|
|
"policy": POLICY,
|
|
"status": status,
|
|
"generated_at": now.isoformat(),
|
|
"artifact_root": str(root),
|
|
"max_age_hours": max_age,
|
|
"platform_filter": list(platforms or []),
|
|
"summary": summary,
|
|
"candidates": replay_candidates,
|
|
"replay_contract": {
|
|
"internal_rag_target": INTERNAL_RAG_TARGET,
|
|
"writes_database": False,
|
|
"writes_ai_insights": False,
|
|
"writes_price_tables": False,
|
|
"network_call": False,
|
|
"secret_read": False,
|
|
"promotion_gate_required_before_knowledge_write": True,
|
|
"ocr_or_vlm_required_before_field_extraction": True,
|
|
"blocked_pages_are_not_product_data": True,
|
|
},
|
|
"next_machine_action": (
|
|
"run_pixelrag_visual_capture_worker"
|
|
if not replay_candidates
|
|
else (
|
|
"fix_invalid_pixelrag_receipts"
|
|
if invalid_count and not eligible_count
|
|
else (
|
|
"run_platform_probe_or_use_structured_api"
|
|
if visual_barrier_count and not eligible_count
|
|
else (
|
|
"run_ocr_vlm_replay_then_promotion_gate"
|
|
if eligible_count
|
|
else "refresh_pixelrag_visual_capture_receipt"
|
|
)
|
|
)
|
|
)
|
|
),
|
|
"controlled_apply": {
|
|
"network_call": False,
|
|
"db_write": False,
|
|
"secret_read": False,
|
|
"production_price_write": False,
|
|
"artifact_write": False,
|
|
},
|
|
}
|
|
|
|
|
|
__all__ = [
|
|
"INTERNAL_RAG_TARGET",
|
|
"POLICY",
|
|
"build_pixelrag_rag_candidate_replay_readback",
|
|
]
|