59 lines
2.6 KiB
Python
59 lines
2.6 KiB
Python
"""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")
|
|
content: list[dict[str, Any]] = [{"type": "text", "text": json.dumps(payload, ensure_ascii=False)}]
|
|
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')}"}}
|