"""Independent rendered-review adapter for protocol v3.""" from __future__ import annotations import base64 import json from pathlib import Path from typing import Any from app.cad_agent.adapters.structured_llm import StructuredModelGateway from app.cad_agent.ports import AdapterUnavailable class RenderedReviewGateway: def __init__(self, models: StructuredModelGateway) -> None: self.models = models async def review(self, *, kind: str, payload: dict[str, Any], tool: dict[str, Any], provider_id: str, model_id: str) -> dict[str, Any]: name = str((tool.get("function") or {}).get("name") or "") if not name: raise RuntimeError("Review tool is missing a name") public_payload = {key: value for key, value in payload.items() if key != "reference_image_paths"} content: list[dict[str, Any]] = [{"type": "text", "text": json.dumps(public_payload, ensure_ascii=False)}] if kind in {"image_observation", "final"}: for raw_path in payload.get("reference_image_paths") or (): path = Path(str(raw_path)) if path.is_file(): content.append(self._image_part(path)) if kind in {"candidate", "final"}: manifest = payload.get("render_manifest") if isinstance(payload.get("render_manifest"), dict) else {} for path in self._evidence_paths(manifest): content.append(self._image_part(path)) return await self.models.call_tool( messages=[ {"role": "system", "content": "You are an independent CAD reviewer. Inspect supplied deterministic facts and rendered images. Return only the specified structured tool call."}, {"role": "user", "content": content}, ], tool=tool, provider_id=provider_id, model_id=model_id, required_tool_name=name, ) @staticmethod def _evidence_paths(manifest: dict[str, Any]) -> list[Path]: selected: list[Path] = [] contact = Path(str(manifest.get("contact_sheet_path") or "")) if contact.is_file(): selected.append(contact) wanted = {"top", "front", "right", "isometric"} for item in manifest.get("views") or (): if not isinstance(item, dict) or str(item.get("id") or "") not in wanted: continue path = Path(str(item.get("path") or "")) if path.is_file(): selected.append(path) if not selected: raise AdapterUnavailable("RENDER_SERVICE_UNAVAILABLE: review render evidence is unavailable") return selected[:5] @staticmethod def _image_part(path: Path) -> dict[str, Any]: media_type = "image/jpeg" if path.suffix.lower() in {".jpg", ".jpeg"} else "image/png" return {"type": "image_url", "image_url": {"url": f"data:{media_type};base64,{base64.b64encode(path.read_bytes()).decode('ascii')}"}}