feat: unify CAD workflows on DesignIR 3.0
This commit is contained in:
@@ -5,6 +5,7 @@ import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -12,6 +13,7 @@ from pathlib import Path
|
||||
|
||||
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
||||
REGISTRY_PATH = Path(__file__).with_name("capabilities.json")
|
||||
RECONSTRUCTION_ADAPTER = Path(__file__).with_name("reconstruct_step.py")
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -335,8 +337,10 @@ def load_experience_library(
|
||||
path: Path | None,
|
||||
family: str | None = None,
|
||||
features: list[str] | None = None,
|
||||
consumer: str = "router",
|
||||
) -> dict[str, object] | None:
|
||||
"""Load promoted generalized methods; reject case parameters and coordinates."""
|
||||
explicit_library = path is not None
|
||||
candidate = path or default_experience_library()
|
||||
if candidate is None:
|
||||
return None
|
||||
@@ -358,15 +362,26 @@ def load_experience_library(
|
||||
or policy.get("single_case_promotion_allowed") is not False
|
||||
):
|
||||
raise ValueError("CAD experience library policy permits instance-answer leakage")
|
||||
if (
|
||||
policy.get("llm_semantic_review_required") is not True
|
||||
or policy.get("draft_evidence_verified") is not True
|
||||
or policy.get("router_consumable") is not True
|
||||
):
|
||||
reviewed = (
|
||||
policy.get("llm_semantic_review_required") is True
|
||||
and policy.get("draft_evidence_verified") is True
|
||||
)
|
||||
consumer_flag = (
|
||||
"surfaceir_consumable"
|
||||
if consumer == "surfaceir"
|
||||
else "router_consumable"
|
||||
)
|
||||
if not reviewed:
|
||||
raise ValueError(
|
||||
"CAD experience library was not produced by LLM semantic review "
|
||||
"plus deterministic evidence verification"
|
||||
)
|
||||
if policy.get(consumer_flag) is not True:
|
||||
if not explicit_library:
|
||||
return None
|
||||
raise ValueError(
|
||||
f"CAD experience library is not approved for {consumer}"
|
||||
)
|
||||
forbidden = _find_forbidden_experience_key(payload)
|
||||
if forbidden:
|
||||
raise ValueError(f"CAD experience library contains forbidden instance key: {forbidden}")
|
||||
@@ -388,6 +403,12 @@ def load_experience_library(
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
scope = set(item.get("scope", []))
|
||||
if consumer == "surfaceir":
|
||||
if scope.intersection(
|
||||
{"surfaceir_reconstruction", "surfaceir_semantic_editing"}
|
||||
):
|
||||
selected.append(item)
|
||||
continue
|
||||
if "global" not in scope:
|
||||
if not compatible_scopes or scope.isdisjoint(compatible_scopes):
|
||||
continue
|
||||
@@ -402,6 +423,11 @@ def load_experience_library(
|
||||
"compatible_scopes": sorted(compatible_scopes),
|
||||
"requested_features": sorted(requested_features),
|
||||
"experiences": selected,
|
||||
"selection_policy": (
|
||||
"surfaceir_runtime_conditioned"
|
||||
if consumer == "surfaceir"
|
||||
else "request_family_and_feature_match"
|
||||
),
|
||||
"policy": {
|
||||
"contains_instance_parameters": False,
|
||||
"contains_absolute_coordinates": False,
|
||||
@@ -429,6 +455,7 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
args.experience_library,
|
||||
experience_family,
|
||||
experience_features,
|
||||
"surfaceir" if teacher_step_reconstruction else "router",
|
||||
)
|
||||
needs_source_classification = bool(edit_context and not experience_family)
|
||||
scores = {
|
||||
@@ -480,7 +507,12 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
|
||||
registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
||||
backend_info = registry["backends"][selected.name]
|
||||
source_of_truth = "per-part DesignIR 2.0 with an isolated backend compiler"
|
||||
editable_contract = "DesignIR 3.0"
|
||||
source_of_truth = (
|
||||
"per-part DesignIR 3.0 with an isolated SurfaceIR compiler"
|
||||
if teacher_step_reconstruction
|
||||
else "per-part semantic DesignIR 3.0 with an isolated backend compiler"
|
||||
)
|
||||
gap = selected.score - max((scores[name].score for name in scores if name != selected.name), default=0)
|
||||
confidence = "high" if gap >= 40 else "medium" if gap >= 15 else "low"
|
||||
result: dict[str, object] = {
|
||||
@@ -499,7 +531,7 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
"workflow_profiles": profiles,
|
||||
"artifact_contract": {
|
||||
"primary_format": "step",
|
||||
"editable_contract": "DesignIR 2.0",
|
||||
"editable_contract": editable_contract,
|
||||
},
|
||||
"source_of_truth": source_of_truth,
|
||||
"experience_context": experience_context,
|
||||
@@ -604,9 +636,72 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
},
|
||||
"availability": probe_backends() if args.probe else None,
|
||||
}
|
||||
if teacher_step_reconstruction:
|
||||
result["execution_adapter"] = {
|
||||
"path": str(RECONSTRUCTION_ADAPTER),
|
||||
"runtime": "DesignIR 3.0 / SurfaceIR",
|
||||
"automatic_geometry_acceptance": True,
|
||||
"automatic_parameter_acceptance": True,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def execute_reconstruction(
|
||||
args: argparse.Namespace, result: dict[str, object]
|
||||
) -> dict[str, object]:
|
||||
edit_context = result.get("design_plan", {}).get("edit_context")
|
||||
if not isinstance(edit_context, dict) or edit_context.get(
|
||||
"modification_mode"
|
||||
) != "private_evidence_to_isolated_designir_reconstruction":
|
||||
raise ValueError(
|
||||
"--execute currently requires --edit-source with one STEP/STP file"
|
||||
)
|
||||
if args.edit_source is None or args.task_dir is None:
|
||||
raise ValueError(
|
||||
"--execute requires both --edit-source and --task-dir"
|
||||
)
|
||||
runtime = os.environ.get("CAD_ROUTER_PYTHON", "").strip()
|
||||
workspace_python = (
|
||||
SKILL_ROOT.parents[2] / "text-to-cad" / ".venv" / "bin" / "python"
|
||||
)
|
||||
interpreter = (
|
||||
Path(runtime).expanduser()
|
||||
if runtime
|
||||
else workspace_python
|
||||
if workspace_python.is_file()
|
||||
else Path(sys.executable)
|
||||
)
|
||||
command = [
|
||||
str(interpreter),
|
||||
str(RECONSTRUCTION_ADAPTER),
|
||||
"--source",
|
||||
str(args.edit_source),
|
||||
"--task-dir",
|
||||
str(args.task_dir),
|
||||
"--request",
|
||||
args.request,
|
||||
"--maximum-parameters",
|
||||
str(args.maximum_parameters),
|
||||
"--timeout-seconds",
|
||||
str(args.timeout_seconds),
|
||||
]
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
detail = completed.stderr.strip() or completed.stdout.strip()
|
||||
raise RuntimeError(
|
||||
"cad-router STEP reconstruction failed: " + detail[-3000:]
|
||||
)
|
||||
lines = [line for line in completed.stdout.splitlines() if line.strip()]
|
||||
if not lines:
|
||||
raise RuntimeError("cad-router reconstruction returned no result")
|
||||
return json.loads(lines[-1])
|
||||
|
||||
|
||||
def explain(result: dict[str, object]) -> str:
|
||||
lines = [
|
||||
f"Selected backend: {result['selected_backend']}",
|
||||
@@ -665,12 +760,36 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
)
|
||||
parser.add_argument("--explain", action="store_true", help="Print a human-readable decision instead of JSON.")
|
||||
parser.add_argument("--manifest", type=Path, help="Also write the route decision as JSON to this explicit path.")
|
||||
parser.add_argument(
|
||||
"--execute",
|
||||
action="store_true",
|
||||
help="Execute an uploaded STEP reconstruction after routing.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--task-dir",
|
||||
type=Path,
|
||||
help="Explicit output directory for --execute.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--maximum-parameters",
|
||||
type=int,
|
||||
default=8,
|
||||
help="Maximum feature-specific edit candidates to validate; overall_scale is also tested as fallback.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout-seconds",
|
||||
type=int,
|
||||
default=180,
|
||||
help="Timeout for each reconstruction or acceptance worker.",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
result = route(args)
|
||||
if args.execute:
|
||||
result["execution"] = execute_reconstruction(args, result)
|
||||
payload = json.dumps(result, ensure_ascii=False, indent=2) + "\n"
|
||||
if args.manifest:
|
||||
args.manifest.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
Reference in New Issue
Block a user