from __future__ import annotations import json import os import re import urllib.error import urllib.request from pathlib import Path from typing import Any from jsonschema import Draft202012Validator from reuse_surface.registry import ROOT DRAFT_SCHEMA_PATH = ROOT / "schemas" / "registry-draft.schema.json" def llm_connect_url(explicit: str | None = None) -> str: base = (explicit or os.environ.get("LLM_CONNECT_URL", "")).rstrip("/") if not base: raise ValueError( "LLM backend not configured; set LLM_CONNECT_URL or pass --llm-url" ) return base def load_draft_schema() -> dict[str, Any]: return json.loads(DRAFT_SCHEMA_PATH.read_text(encoding="utf-8")) def execute_prompt( prompt: str, *, base_url: str | None = None, config: dict[str, Any] | None = None, ) -> str: url = f"{llm_connect_url(base_url)}/execute" body: dict[str, Any] = {"prompt": prompt} if config: body["config"] = config data = json.dumps(body).encode("utf-8") request = urllib.request.Request( url, data=data, headers={ "Content-Type": "application/json", "Accept": "application/json", "User-Agent": "reuse-surface/0.1", }, method="POST", ) try: with urllib.request.urlopen(request, timeout=120) as response: payload = json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as exc: raw = exc.read().decode("utf-8") raise ValueError(f"llm-connect returned {exc.code}: {raw}") from exc content = payload.get("content") if not isinstance(content, str) or not content.strip(): raise ValueError("llm-connect response missing content") return content def extract_json_object(text: str) -> dict[str, Any]: stripped = text.strip() if stripped.startswith("```"): stripped = re.sub(r"^```(?:json)?\s*", "", stripped) stripped = re.sub(r"\s*```$", "", stripped) try: data = json.loads(stripped) except json.JSONDecodeError: match = re.search(r"\{.*\}", stripped, re.DOTALL) if not match: raise ValueError("llm response did not contain JSON object") from None data = json.loads(match.group(0)) if not isinstance(data, dict): raise ValueError("llm response JSON must be an object") return data def request_registry_draft( prompt: str, *, base_url: str | None = None, config: dict[str, Any] | None = None, ) -> dict[str, Any]: draft = extract_json_object(execute_prompt(prompt, base_url=base_url, config=config)) validator = Draft202012Validator(load_draft_schema()) errors = sorted(validator.iter_errors(draft), key=lambda err: list(err.path)) if errors: messages = "; ".join(error.message for error in errors[:3]) raise ValueError(f"draft schema validation failed: {messages}") return draft def request_json_object( prompt: str, *, base_url: str | None = None, config: dict[str, Any] | None = None, ) -> dict[str, Any]: return extract_json_object(execute_prompt(prompt, base_url=base_url, config=config))