feat: integrate SimpleCADAPI 2.0.2 CAD workflows
This commit is contained in:
@@ -1,5 +1,21 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"schema_version": "2.0",
|
||||
"routing_policy": {
|
||||
"default_backend": null,
|
||||
"selection_basis": [
|
||||
"native source continuity",
|
||||
"standard factory coverage",
|
||||
"semantic and replay requirements",
|
||||
"geometry vocabulary coverage",
|
||||
"assembly requirements",
|
||||
"validation requirements"
|
||||
],
|
||||
"uploaded_step_execution_engine": "surfaceir",
|
||||
"uploaded_step_validation_engines": [
|
||||
"surfaceir_geometry_acceptance",
|
||||
"simplecadapi_brep_compare"
|
||||
]
|
||||
},
|
||||
"backends": {
|
||||
"build123d": {
|
||||
"project": "text-to-cad",
|
||||
@@ -10,21 +26,43 @@
|
||||
"general mechanical parts",
|
||||
"machined features",
|
||||
"source-level assemblies",
|
||||
"existing STEP inspection and modification",
|
||||
"freeform profiles, lofts, sweeps, and mixed surface workflows",
|
||||
"geometry measurement and validation"
|
||||
],
|
||||
"preferred_when": [
|
||||
"the part is a custom bracket, housing, shaft, flange, fixture, or enclosure",
|
||||
"source-level assembly mates or existing build123d Python must be preserved",
|
||||
"the geometry needs custom profiles or surface-heavy construction not covered by a SimpleCADAPI factory"
|
||||
],
|
||||
"weak_when": [
|
||||
"a deterministic SimpleCADAPI standard factory covers the requested part",
|
||||
"operation-graph replay, typed semantic tags, units, or tolerance chains are primary requirements"
|
||||
],
|
||||
"source_of_truth": "build123d Python generator"
|
||||
},
|
||||
"simplecadapi": {
|
||||
"project": "SimpleCADAPI",
|
||||
"runner_skill": "simplecadapi",
|
||||
"minimum_version": "2.0.2",
|
||||
"source_formats": ["py", "json"],
|
||||
"primary_formats": ["step", "stp"],
|
||||
"strengths": [
|
||||
"gears, ring gears, racks, and cycloidal discs",
|
||||
"bearings, reducers, and joint actuators",
|
||||
"replayable operation graphs",
|
||||
"semantic tags and topology queries"
|
||||
"gears, ring gears, racks, bevel gears, and cycloidal discs",
|
||||
"bearings, bolts, nuts, roller-chain sprockets, reducers, and joint actuators",
|
||||
"replayable ModelResult operation graphs and scene packages",
|
||||
"typed semantic tags, topology lineage, and source mapping",
|
||||
"physical units and tolerance-chain validation",
|
||||
"STEP B-Rep inspection, strict comparison, and slice diagnostics",
|
||||
"FreeCAD, Fusion 360, and SolidWorks translation"
|
||||
],
|
||||
"preferred_when": [
|
||||
"a standard mechanical factory directly covers the requested family",
|
||||
"replayable model JSON, semantic edit anchors, units, tolerances, or CAD translation are required",
|
||||
"a reusable mechanical product family should become a deterministic library function"
|
||||
],
|
||||
"weak_when": [
|
||||
"the request depends on complex freeform surfaces outside the supported operation vocabulary",
|
||||
"an existing build123d generator or source-level build123d assembly must be preserved"
|
||||
],
|
||||
"source_of_truth": "SimpleCADAPI Python generator and model JSON"
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
@@ -34,6 +35,11 @@ DESIGNIR_SCHEMA = (
|
||||
/ "contracts"
|
||||
/ "designir-3.0.schema.json"
|
||||
)
|
||||
DESIGNIR_SCRIPTS = WORKSPACE_ROOT / "designir-pipeline" / "scripts"
|
||||
if str(DESIGNIR_SCRIPTS) not in sys.path:
|
||||
sys.path.insert(0, str(DESIGNIR_SCRIPTS))
|
||||
|
||||
from designir_codec import read_designir, write_designir
|
||||
|
||||
EXECUTABLE_EDIT_STATES = {
|
||||
"executable_enlarge_cut_binding",
|
||||
@@ -80,10 +86,14 @@ def _safe_stem(source: Path) -> str:
|
||||
|
||||
|
||||
def _write_json(path: Path, payload: dict[str, Any]) -> None:
|
||||
if path.name.endswith(".designir.json"):
|
||||
write_designir(path, payload)
|
||||
return
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(
|
||||
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
|
||||
|
||||
@@ -117,6 +127,66 @@ def _run_json(
|
||||
) from exc
|
||||
|
||||
|
||||
def _run_simplecad_brep_compare(
|
||||
runtime: Path,
|
||||
source: Path,
|
||||
rebuilt: Path,
|
||||
report_path: Path,
|
||||
*,
|
||||
timeout_seconds: int,
|
||||
) -> dict[str, Any]:
|
||||
"""Run the installed SimpleCADAPI comparator in the acceptance zone.
|
||||
|
||||
A topology mismatch is a valid diagnostic result, not a worker failure.
|
||||
The teacher path is provided only to this acceptance subprocess.
|
||||
"""
|
||||
command = [
|
||||
str(runtime),
|
||||
"-m",
|
||||
"simplecadapi.inverse_engineer.brep.cli",
|
||||
"compare",
|
||||
str(source),
|
||||
str(rebuilt),
|
||||
"--output",
|
||||
str(report_path),
|
||||
]
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout_seconds,
|
||||
check=False,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
return {
|
||||
"status": "unavailable",
|
||||
"reason": "timeout",
|
||||
"error": str(exc),
|
||||
}
|
||||
if report_path.is_file():
|
||||
try:
|
||||
payload = json.loads(report_path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
return {
|
||||
"status": "unavailable",
|
||||
"reason": "invalid_report",
|
||||
"error": str(exc),
|
||||
}
|
||||
payload["status"] = (
|
||||
"passed" if payload.get("hard_gate_passed") else "mismatch"
|
||||
)
|
||||
payload["engine"] = "simplecadapi.inverse_engineer.brep"
|
||||
return payload
|
||||
detail = completed.stderr.strip() or completed.stdout.strip()
|
||||
return {
|
||||
"status": "unavailable",
|
||||
"reason": "worker_failed",
|
||||
"return_code": completed.returncode,
|
||||
"error": detail[-2000:],
|
||||
}
|
||||
|
||||
|
||||
def _validate_designir_contract(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
required = {
|
||||
"schema_version",
|
||||
@@ -261,7 +331,7 @@ def _persist_edit_results(
|
||||
designir_path: Path,
|
||||
attempts: list[dict[str, Any]],
|
||||
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
||||
payload = json.loads(designir_path.read_text(encoding="utf-8"))
|
||||
payload = read_designir(designir_path)
|
||||
parameters = payload.get("semantic_layer", {}).get("parameters", {})
|
||||
accepted_parameters: list[dict[str, Any]] = []
|
||||
attempted_names = set()
|
||||
@@ -312,6 +382,7 @@ def reconstruct_uploaded_step(
|
||||
request: str,
|
||||
maximum_parameters: int = 8,
|
||||
timeout_seconds: int = 180,
|
||||
semantic_backend: str = "build123d",
|
||||
) -> dict[str, Any]:
|
||||
source = source.expanduser().resolve()
|
||||
task_dir = task_dir.expanduser().resolve()
|
||||
@@ -338,6 +409,7 @@ def reconstruct_uploaded_step(
|
||||
rebuilt_path = task_dir / f"{stem}.reconstructed.step"
|
||||
validation_dir = task_dir / "validation"
|
||||
geometry_report_path = validation_dir / "geometry.json"
|
||||
brep_report_path = validation_dir / "simplecad-brep.json"
|
||||
edit_report_path = validation_dir / "edits.json"
|
||||
parameters_path = task_dir / "parameters.json"
|
||||
task_manifest_path = task_dir / "cad-task.json"
|
||||
@@ -360,7 +432,7 @@ def reconstruct_uploaded_step(
|
||||
runtime = _runtime_python()
|
||||
common = [str(runtime), str(SURFACEIR_PIPELINE)]
|
||||
available_experience_ids = _surfaceir_experience_ids()
|
||||
boundary_strategy = (
|
||||
preferred_boundary_strategy = (
|
||||
"exact_3d_pcurve"
|
||||
if (
|
||||
"failure_repair.surface_topology.bind_face_pcurves_and_group_shells"
|
||||
@@ -380,25 +452,9 @@ def reconstruct_uploaded_step(
|
||||
timeout_seconds=timeout_seconds,
|
||||
stage="DesignIR 3.0 extraction",
|
||||
)
|
||||
payload = json.loads(designir_path.read_text(encoding="utf-8"))
|
||||
payload = read_designir(designir_path)
|
||||
contract_checks = _validate_designir_contract(payload)
|
||||
|
||||
# The rebuild worker receives no teacher path. It can regenerate the model
|
||||
# from the DesignIR artifact alone.
|
||||
_run_json(
|
||||
common
|
||||
+ [
|
||||
"rebuild",
|
||||
str(designir_path),
|
||||
"--output",
|
||||
str(rebuilt_path),
|
||||
"--boundary-strategy",
|
||||
boundary_strategy,
|
||||
],
|
||||
timeout_seconds=timeout_seconds,
|
||||
stage="source-independent SurfaceIR rebuild",
|
||||
)
|
||||
|
||||
manifest_path = task_dir / "manifest.json"
|
||||
_write_json(
|
||||
manifest_path,
|
||||
@@ -415,35 +471,162 @@ def reconstruct_uploaded_step(
|
||||
},
|
||||
)
|
||||
validation_dir.mkdir(parents=True, exist_ok=True)
|
||||
geometry_work_dir = validation_dir / ".geometry-work"
|
||||
geometry_result = _run_json(
|
||||
common
|
||||
+ [
|
||||
"validate-folder",
|
||||
str(source.parent),
|
||||
"--designir-dir",
|
||||
str(task_dir),
|
||||
"--rebuilt-dir",
|
||||
str(geometry_work_dir),
|
||||
"--limit",
|
||||
"1",
|
||||
"--boolean",
|
||||
"--boundary-strategy",
|
||||
boundary_strategy,
|
||||
],
|
||||
timeout_seconds=timeout_seconds,
|
||||
stage="Acceptance-Agent geometry validation",
|
||||
strategy_root = validation_dir / ".strategy-work"
|
||||
strategies = [preferred_boundary_strategy]
|
||||
if (
|
||||
"failure_repair.semantic_edit.boundary_strategy_fallback"
|
||||
in available_experience_ids
|
||||
):
|
||||
strategies.extend(
|
||||
strategy
|
||||
for strategy in (
|
||||
"exact_3d",
|
||||
"first_pcurve",
|
||||
"analytic_uv_rect",
|
||||
"uv_rect_all",
|
||||
)
|
||||
if strategy not in strategies
|
||||
)
|
||||
|
||||
strategy_attempts: list[dict[str, Any]] = []
|
||||
selected: tuple[
|
||||
tuple[float, ...],
|
||||
str,
|
||||
dict[str, Any],
|
||||
dict[str, Any],
|
||||
Path,
|
||||
] | None = None
|
||||
for strategy in strategies:
|
||||
attempt_dir = strategy_root / strategy
|
||||
try:
|
||||
candidate_result = _run_json(
|
||||
common
|
||||
+ [
|
||||
"validate-folder",
|
||||
str(source.parent),
|
||||
"--designir-dir",
|
||||
str(task_dir),
|
||||
"--rebuilt-dir",
|
||||
str(attempt_dir),
|
||||
"--limit",
|
||||
"1",
|
||||
"--boolean",
|
||||
"--boundary-strategy",
|
||||
strategy,
|
||||
],
|
||||
timeout_seconds=timeout_seconds,
|
||||
stage=f"Acceptance-Agent geometry validation ({strategy})",
|
||||
)
|
||||
report = json.loads(
|
||||
(attempt_dir / "validation-report.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
row = (report.get("rows") or [{}])[0]
|
||||
candidate_steps = sorted(attempt_dir.glob("*.step"))
|
||||
if not candidate_steps:
|
||||
raise ReconstructionError(
|
||||
f"Boundary strategy {strategy} produced no rebuilt STEP"
|
||||
)
|
||||
|
||||
def finite_metric(name: str) -> float:
|
||||
value = row.get(name)
|
||||
if isinstance(value, (int, float)) and math.isfinite(value):
|
||||
return float(value)
|
||||
return math.inf
|
||||
|
||||
score = (
|
||||
0.0
|
||||
if row.get("geometry_pass")
|
||||
else 1.0
|
||||
if row.get("usable_reconstruction")
|
||||
else 2.0,
|
||||
0.0 if row.get("topology_match") else 1.0,
|
||||
finite_metric("relative_volume_error"),
|
||||
finite_metric("relative_surface_area_error"),
|
||||
finite_metric("center_of_mass_error"),
|
||||
finite_metric("bounds_error"),
|
||||
finite_metric("bidirectional_sample_distance"),
|
||||
)
|
||||
strategy_attempts.append(
|
||||
{
|
||||
"strategy": strategy,
|
||||
"status": "evaluated",
|
||||
"score": list(score),
|
||||
"geometry_pass": bool(row.get("geometry_pass")),
|
||||
"usable_reconstruction": bool(
|
||||
row.get("usable_reconstruction")
|
||||
),
|
||||
"topology_match": bool(row.get("topology_match")),
|
||||
"relative_volume_error": row.get("relative_volume_error"),
|
||||
"relative_surface_area_error": row.get(
|
||||
"relative_surface_area_error"
|
||||
),
|
||||
"center_of_mass_error": row.get("center_of_mass_error"),
|
||||
"bounds_error": row.get("bounds_error"),
|
||||
"bidirectional_sample_distance": row.get(
|
||||
"bidirectional_sample_distance"
|
||||
),
|
||||
}
|
||||
)
|
||||
candidate = (
|
||||
score,
|
||||
strategy,
|
||||
report,
|
||||
candidate_result,
|
||||
candidate_steps[0],
|
||||
)
|
||||
if selected is None or candidate[0] < selected[0]:
|
||||
selected = candidate
|
||||
except (ReconstructionError, subprocess.TimeoutExpired) as exc:
|
||||
strategy_attempts.append(
|
||||
{
|
||||
"strategy": strategy,
|
||||
"status": "worker_failed",
|
||||
"error": str(exc),
|
||||
}
|
||||
)
|
||||
|
||||
strategy_report_path = validation_dir / "strategies.json"
|
||||
if selected is None:
|
||||
_write_json(
|
||||
strategy_report_path,
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"selected_strategy": None,
|
||||
"attempts": strategy_attempts,
|
||||
},
|
||||
)
|
||||
raise ReconstructionError("Every SurfaceIR boundary strategy failed")
|
||||
|
||||
_, boundary_strategy, geometry_report, geometry_result, selected_step = selected
|
||||
shutil.copy2(selected_step, rebuilt_path)
|
||||
_write_json(
|
||||
strategy_report_path,
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"selected_strategy": boundary_strategy,
|
||||
"attempts": strategy_attempts,
|
||||
},
|
||||
)
|
||||
raw_geometry_report = geometry_work_dir / "validation-report.json"
|
||||
geometry_report = json.loads(raw_geometry_report.read_text(encoding="utf-8"))
|
||||
_write_json(geometry_report_path, geometry_report)
|
||||
geometry_pass = geometry_report.get("geometry_pass_count") == 1
|
||||
if not geometry_pass:
|
||||
usable_reconstruction = (
|
||||
geometry_report.get("usable_reconstruction_count") == 1
|
||||
)
|
||||
if not geometry_pass and not usable_reconstruction:
|
||||
raise ReconstructionError(
|
||||
"Independent reconstruction did not satisfy the geometry contract; "
|
||||
f"see {geometry_report_path}"
|
||||
)
|
||||
shutil.rmtree(geometry_work_dir)
|
||||
brep_comparison = _run_simplecad_brep_compare(
|
||||
runtime,
|
||||
source,
|
||||
rebuilt_path,
|
||||
brep_report_path,
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
shutil.rmtree(strategy_root)
|
||||
|
||||
attempts: list[dict[str, Any]] = []
|
||||
if payload["document_status"] == "geometry_present":
|
||||
@@ -502,7 +685,9 @@ def reconstruct_uploaded_step(
|
||||
)
|
||||
payload["reconstruction_strategy"] = {
|
||||
"boundary_strategy": boundary_strategy,
|
||||
"selection_status": "geometry_validated",
|
||||
"selection_status": (
|
||||
"geometry_validated" if geometry_pass else "unresolved"
|
||||
),
|
||||
}
|
||||
_write_json(designir_path, payload)
|
||||
applied_experience_ids = _applied_experience_ids(
|
||||
@@ -525,6 +710,41 @@ def reconstruct_uploaded_step(
|
||||
"policy": "Only parameters that passed an isolated perturbation test are exposed.",
|
||||
}
|
||||
_write_json(parameters_path, parameter_catalog)
|
||||
brep_hard_gate_passed = brep_comparison.get("hard_gate_passed") is True
|
||||
brep_available = brep_comparison.get("status") in {"passed", "mismatch"}
|
||||
quality_tier = (
|
||||
"brep_exact"
|
||||
if geometry_pass and brep_hard_gate_passed
|
||||
else "geometry_summary_accepted_brep_mismatch"
|
||||
if geometry_pass and brep_available
|
||||
else "geometry_accepted_brep_unavailable"
|
||||
if geometry_pass
|
||||
else "review_required"
|
||||
)
|
||||
published_artifacts = [
|
||||
{"path": rebuilt_path.name, "role": "primary_step"},
|
||||
{"path": designir_path.name, "role": "editable_designir"},
|
||||
{"path": parameters_path.name, "role": "validated_parameters"},
|
||||
{
|
||||
"path": str(geometry_report_path.relative_to(task_dir)),
|
||||
"role": "geometry_acceptance",
|
||||
},
|
||||
{
|
||||
"path": str(edit_report_path.relative_to(task_dir)),
|
||||
"role": "parameter_acceptance",
|
||||
},
|
||||
{
|
||||
"path": str(strategy_report_path.relative_to(task_dir)),
|
||||
"role": "boundary_strategy_acceptance",
|
||||
},
|
||||
]
|
||||
if brep_report_path.is_file():
|
||||
published_artifacts.append(
|
||||
{
|
||||
"path": str(brep_report_path.relative_to(task_dir)),
|
||||
"role": "strict_brep_acceptance",
|
||||
}
|
||||
)
|
||||
|
||||
task_manifest = {
|
||||
"schema_version": "1.0",
|
||||
@@ -533,8 +753,12 @@ def reconstruct_uploaded_step(
|
||||
"route": {
|
||||
"router": "cad-router",
|
||||
"runtime": "DesignIR 3.0 / SurfaceIR",
|
||||
"selected_backend": "build123d",
|
||||
"selected_backend": semantic_backend,
|
||||
"execution_adapter": "surfaceir_occt",
|
||||
"validation_adapters": [
|
||||
"surfaceir_geometry_acceptance",
|
||||
"simplecadapi_brep_compare",
|
||||
],
|
||||
"source_of_truth": designir_path.name,
|
||||
},
|
||||
"source": {
|
||||
@@ -542,19 +766,7 @@ def reconstruct_uploaded_step(
|
||||
"role": "teacher_and_acceptance_truth_only",
|
||||
"embedded_in_output": False,
|
||||
},
|
||||
"artifacts": [
|
||||
{"path": rebuilt_path.name, "role": "primary_step"},
|
||||
{"path": designir_path.name, "role": "editable_designir"},
|
||||
{"path": parameters_path.name, "role": "validated_parameters"},
|
||||
{
|
||||
"path": str(geometry_report_path.relative_to(task_dir)),
|
||||
"role": "geometry_acceptance",
|
||||
},
|
||||
{
|
||||
"path": str(edit_report_path.relative_to(task_dir)),
|
||||
"role": "parameter_acceptance",
|
||||
},
|
||||
],
|
||||
"artifacts": published_artifacts,
|
||||
"experience": {
|
||||
"library": (
|
||||
str(EXPERIENCE_LIBRARY)
|
||||
@@ -566,7 +778,11 @@ def reconstruct_uploaded_step(
|
||||
"methods_applied": applied_experience_ids,
|
||||
},
|
||||
"validation": {
|
||||
"geometry_accepted": True,
|
||||
"geometry_accepted": geometry_pass,
|
||||
"geometry_usable": usable_reconstruction,
|
||||
"brep_comparison_status": brep_comparison.get("status"),
|
||||
"brep_hard_gate_passed": brep_hard_gate_passed,
|
||||
"quality_tier": quality_tier,
|
||||
"source_independent_rebuild": True,
|
||||
"anti_cheat_pass": True,
|
||||
"validated_parameter_count": len(accepted_parameters),
|
||||
@@ -592,7 +808,15 @@ def reconstruct_uploaded_step(
|
||||
return {
|
||||
"schema_version": "1.0",
|
||||
"status": (
|
||||
"accepted"
|
||||
(
|
||||
"accepted"
|
||||
if geometry_pass
|
||||
and (
|
||||
brep_hard_gate_passed
|
||||
or brep_comparison.get("status") == "unavailable"
|
||||
)
|
||||
else "review_required"
|
||||
)
|
||||
if payload["document_status"] == "geometry_present"
|
||||
else "accepted_empty_source"
|
||||
),
|
||||
@@ -603,7 +827,13 @@ def reconstruct_uploaded_step(
|
||||
"task_manifest": str(task_manifest_path),
|
||||
"geometry_report": str(geometry_report_path),
|
||||
"edit_report": str(edit_report_path),
|
||||
"geometry_accepted": True,
|
||||
"geometry_accepted": geometry_pass,
|
||||
"geometry_usable": usable_reconstruction,
|
||||
"brep_comparison": brep_comparison,
|
||||
"brep_report": (
|
||||
str(brep_report_path) if brep_report_path.is_file() else None
|
||||
),
|
||||
"quality_tier": quality_tier,
|
||||
"document_status": payload["document_status"],
|
||||
"validated_parameter_count": len(accepted_parameters),
|
||||
"validated_parameter_names": [
|
||||
@@ -620,6 +850,10 @@ def reconstruct_uploaded_step(
|
||||
"stages": {
|
||||
"extraction": extraction,
|
||||
"geometry_validation": geometry_result,
|
||||
"brep_validation": {
|
||||
"engine": "SimpleCADAPI 2.0.2",
|
||||
"status": brep_comparison.get("status"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -631,6 +865,12 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument("--request", default="重建上传的 STEP")
|
||||
parser.add_argument("--maximum-parameters", type=int, default=8)
|
||||
parser.add_argument("--timeout-seconds", type=int, default=180)
|
||||
parser.add_argument(
|
||||
"--semantic-backend",
|
||||
choices=("build123d", "simplecadapi"),
|
||||
default="build123d",
|
||||
help="Preferred editable semantic backend after SurfaceIR reconstruction.",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -643,6 +883,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
request=args.request,
|
||||
maximum_parameters=args.maximum_parameters,
|
||||
timeout_seconds=args.timeout_seconds,
|
||||
semantic_backend=args.semantic_backend,
|
||||
)
|
||||
except (ReconstructionError, subprocess.TimeoutExpired) as exc:
|
||||
print(
|
||||
|
||||
@@ -5,9 +5,11 @@ import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -28,19 +30,31 @@ class BackendScore:
|
||||
|
||||
|
||||
KEYWORD_GROUPS: dict[str, tuple[str, ...]] = {
|
||||
"special_mechanical": (
|
||||
"standard_factory": (
|
||||
"gear", "gears", "齿轮", "齿条", "rack", "ring gear", "内齿圈",
|
||||
"cycloidal", "摆线", "bearing", "轴承", "planetary", "行星",
|
||||
"reducer", "gearbox", "减速器", "减速箱", "joint actuator", "关节执行器",
|
||||
"bevel gear", "锥齿轮", "sprocket", "链轮", "roller chain", "滚子链",
|
||||
"bolt", "螺栓", "nut", "螺母", "fastener", "紧固件",
|
||||
),
|
||||
"general_mechanical": (
|
||||
"bracket", "支架", "connecting rod", "连杆", "crank", "曲柄",
|
||||
"lever", "摇臂", "shaft", "轴", "bushing", "轴套", "flange", "法兰",
|
||||
"housing", "壳体", "enclosure", "外壳", "fixture", "夹具", "plate", "安装板",
|
||||
),
|
||||
"replay": (
|
||||
"semantic_modeling": (
|
||||
"replay", "重放", "operation graph", "model json", "模型图", "semantic tag",
|
||||
"语义标签",
|
||||
"语义标签", "topology", "拓扑追踪", "source mapping", "来源映射",
|
||||
"unit", "单位", "tolerance", "公差", "公差链", "scene.zip",
|
||||
"freecad", "fusion 360", "solidworks",
|
||||
),
|
||||
"custom_surface": (
|
||||
"freeform", "自由曲面", "organic", "有机曲面", "nurbs", "复杂曲面",
|
||||
"surface patch", "曲面补片", "multi rail", "多轨扫掠",
|
||||
),
|
||||
"mechanism": (
|
||||
"mechanism", "机构", "motion", "运动关系", "gear constraint", "齿轮约束",
|
||||
"belt constraint", "皮带约束", "revolute", "转动副", "prismatic", "移动副",
|
||||
),
|
||||
"visual_complexity": (
|
||||
"image", "photo", "sketch", "图片", "照片", "草图", "复杂", "complex",
|
||||
@@ -186,18 +200,98 @@ def infer_experience_query(request: str) -> tuple[str | None, list[str]]:
|
||||
|
||||
|
||||
def probe_backends() -> dict[str, dict[str, object]]:
|
||||
def distribution_version(name: str) -> str | None:
|
||||
try:
|
||||
return version(name)
|
||||
except PackageNotFoundError:
|
||||
return None
|
||||
|
||||
simplecad_version = distribution_version("simplecadapi")
|
||||
return {
|
||||
"build123d": {
|
||||
"available": importlib.util.find_spec("build123d") is not None,
|
||||
"version": distribution_version("build123d"),
|
||||
"probe": f"python={sys.executable}",
|
||||
},
|
||||
"simplecadapi": {
|
||||
"available": importlib.util.find_spec("simplecadapi") is not None,
|
||||
"version": simplecad_version,
|
||||
"minimum_version": "2.0.2",
|
||||
"compatible": bool(
|
||||
simplecad_version
|
||||
and tuple(
|
||||
int(part)
|
||||
for part in re.findall(r"\d+", simplecad_version)[:3]
|
||||
)
|
||||
>= (2, 0, 2)
|
||||
),
|
||||
"probe": f"python={sys.executable}",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _source_backend_hint(edit_source: Path | None) -> str | None:
|
||||
"""Read only an explicitly supplied editable source to preserve its backend."""
|
||||
if edit_source is None:
|
||||
return None
|
||||
source = edit_source.expanduser()
|
||||
if not source.is_file():
|
||||
return None
|
||||
if source.name.endswith(".designir.json"):
|
||||
try:
|
||||
payload = json.loads(source.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return None
|
||||
hint = payload.get("backend_hint")
|
||||
if hint in {"build123d", "simplecadapi"}:
|
||||
return hint
|
||||
compiler = payload.get("compiler")
|
||||
if isinstance(compiler, dict) and compiler.get("backend") in {
|
||||
"build123d",
|
||||
"simplecadapi",
|
||||
}:
|
||||
return str(compiler["backend"])
|
||||
if source.suffix.lower() == ".py":
|
||||
try:
|
||||
prefix = source.read_text(encoding="utf-8")[:100_000].lower()
|
||||
except OSError:
|
||||
return None
|
||||
if "import simplecadapi" in prefix or "from simplecadapi" in prefix:
|
||||
return "simplecadapi"
|
||||
if "import build123d" in prefix or "from build123d" in prefix:
|
||||
return "build123d"
|
||||
return None
|
||||
|
||||
|
||||
def _routing_requirements(
|
||||
text: str,
|
||||
*,
|
||||
edit_context: dict[str, object] | None,
|
||||
assembly: bool,
|
||||
source_backend: str | None,
|
||||
) -> list[str]:
|
||||
requirements = ["step_export", "editable_designir"]
|
||||
if contains_any(text, KEYWORD_GROUPS["standard_factory"]):
|
||||
requirements.append("standard_mechanical_factory")
|
||||
if contains_any(text, KEYWORD_GROUPS["semantic_modeling"]):
|
||||
requirements.append("semantic_graph_and_replay")
|
||||
if contains_any(text, KEYWORD_GROUPS["custom_surface"]):
|
||||
requirements.append("custom_surface_vocabulary")
|
||||
if contains_any(text, KEYWORD_GROUPS["mechanism"]):
|
||||
requirements.append("mechanism_constraints")
|
||||
if assembly:
|
||||
requirements.append("assembly")
|
||||
if source_backend:
|
||||
requirements.append(f"preserve_{source_backend}_source")
|
||||
if edit_context and edit_context.get("modification_mode") == (
|
||||
"private_evidence_to_isolated_designir_reconstruction"
|
||||
):
|
||||
requirements.extend(
|
||||
["source_independent_surfaceir_rebuild", "strict_brep_validation"]
|
||||
)
|
||||
return list(dict.fromkeys(requirements))
|
||||
|
||||
|
||||
EXPERIENCE_FORBIDDEN_KEYS = {
|
||||
"parameters",
|
||||
"parameter_examples",
|
||||
@@ -458,34 +552,125 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
"surfaceir" if teacher_step_reconstruction else "router",
|
||||
)
|
||||
needs_source_classification = bool(edit_context and not experience_family)
|
||||
source_backend = _source_backend_hint(args.edit_source)
|
||||
requirements = _routing_requirements(
|
||||
text,
|
||||
edit_context=edit_context,
|
||||
assembly=args.assembly,
|
||||
source_backend=source_backend,
|
||||
)
|
||||
scores = {
|
||||
"build123d": BackendScore("build123d", 35, ["+35 general STEP-first default"]),
|
||||
"simplecadapi": BackendScore("simplecadapi", 20, ["+20 specialist mechanical baseline"]),
|
||||
"build123d": BackendScore(
|
||||
"build123d",
|
||||
30,
|
||||
["+30 broad custom mechanical geometry coverage"],
|
||||
),
|
||||
"simplecadapi": BackendScore(
|
||||
"simplecadapi",
|
||||
30,
|
||||
["+30 replayable OCP-native parametric coverage"],
|
||||
),
|
||||
}
|
||||
availability = probe_backends() if args.probe else None
|
||||
if availability:
|
||||
for backend_name, facts in availability.items():
|
||||
if facts.get("available") is not True:
|
||||
scores[backend_name].add(
|
||||
-10_000, "backend is unavailable in the probed runtime"
|
||||
)
|
||||
if availability["simplecadapi"].get("compatible") is not True:
|
||||
scores["simplecadapi"].add(
|
||||
-10_000,
|
||||
"installed SimpleCADAPI does not satisfy the 2.0.2 capability contract",
|
||||
)
|
||||
|
||||
if contains_any(text, KEYWORD_GROUPS["special_mechanical"]):
|
||||
scores["simplecadapi"].add(65, "supported standard mechanical family")
|
||||
scores["build123d"].add(8, "can model the family generically")
|
||||
if contains_any(text, KEYWORD_GROUPS["standard_factory"]):
|
||||
scores["simplecadapi"].add(
|
||||
85, "a deterministic standard mechanical factory is available"
|
||||
)
|
||||
scores["build123d"].add(10, "the family can be modeled generically")
|
||||
if contains_any(text, KEYWORD_GROUPS["general_mechanical"]):
|
||||
scores["build123d"].add(35, "general mechanical part with direct parametric features")
|
||||
scores["simplecadapi"].add(10, "general OCP modeling is available")
|
||||
if contains_any(text, KEYWORD_GROUPS["replay"]):
|
||||
scores["simplecadapi"].add(60, "replay or semantic graph requirement")
|
||||
scores["build123d"].add(
|
||||
32, "custom mechanical part benefits from broad feature construction"
|
||||
)
|
||||
scores["simplecadapi"].add(
|
||||
24, "general OCP feature modeling and semantic tagging are available"
|
||||
)
|
||||
if contains_any(text, KEYWORD_GROUPS["semantic_modeling"]):
|
||||
scores["simplecadapi"].add(
|
||||
70,
|
||||
"model replay, semantic lineage, units, tolerances, or translation is required",
|
||||
)
|
||||
scores["build123d"].add(
|
||||
8, "editable Python can preserve parameters but has no equivalent unified graph"
|
||||
)
|
||||
if contains_any(text, KEYWORD_GROUPS["custom_surface"]):
|
||||
scores["build123d"].add(
|
||||
65, "custom/freeform surface vocabulary favors the general CAD backend"
|
||||
)
|
||||
scores["simplecadapi"].add(
|
||||
-20, "requested surface vocabulary may exceed deterministic SDK coverage"
|
||||
)
|
||||
if contains_any(text, KEYWORD_GROUPS["mechanism"]):
|
||||
scores["simplecadapi"].add(
|
||||
38, "typed mechanism constraints and replayable products are available"
|
||||
)
|
||||
scores["build123d"].add(
|
||||
24, "source-level joints and assemblies are available"
|
||||
)
|
||||
|
||||
if edit_context:
|
||||
scores["build123d"].add(55, "existing model inspection or source modification")
|
||||
if source_backend:
|
||||
scores[source_backend].add(
|
||||
120, f"preserve the explicitly supplied {source_backend} source of truth"
|
||||
)
|
||||
other = "simplecadapi" if source_backend == "build123d" else "build123d"
|
||||
scores[other].add(
|
||||
-35, "backend conversion would risk feature-history and edit-contract loss"
|
||||
)
|
||||
elif edit_context and not teacher_step_reconstruction:
|
||||
scores["build123d"].add(
|
||||
20, "general existing-source inspection and local feature repair"
|
||||
)
|
||||
scores["simplecadapi"].add(
|
||||
20, "semantic graph editing is available when the source supports it"
|
||||
)
|
||||
if teacher_step_reconstruction:
|
||||
# Uploaded STEP reconstruction is executed by SurfaceIR. Backend scores
|
||||
# describe the preferred semantic authoring backend after reconstruction,
|
||||
# not the source-independent rebuild engine itself.
|
||||
scores["build123d"].add(
|
||||
18, "broad semantic feature vocabulary can author future repairs"
|
||||
)
|
||||
scores["simplecadapi"].add(
|
||||
18, "strict B-Rep comparison and semantic lineage strengthen acceptance"
|
||||
)
|
||||
if args.assembly:
|
||||
scores["build123d"].add(15, "source-level assembly workflow")
|
||||
scores["simplecadapi"].add(18, "mechanism and constraint workflow")
|
||||
scores["build123d"].add(35, "editable STEP is the fixed artifact contract")
|
||||
scores["build123d"].add(25, "source-level assembly and datum workflow")
|
||||
scores["simplecadapi"].add(
|
||||
25, "replayable product, connector, and constraint workflow"
|
||||
)
|
||||
scores["build123d"].add(15, "STEP export and editable Python satisfy the contract")
|
||||
scores["simplecadapi"].add(
|
||||
30, "OCP STEP export satisfies the fixed artifact contract"
|
||||
15, "STEP export plus model JSON satisfy the contract"
|
||||
)
|
||||
if experience_context and experience_context["experiences"]:
|
||||
scores["build123d"].add(18, "generalized experience can guide parametric feature composition")
|
||||
scores["simplecadapi"].add(18, "generalized experience can guide semantic feature composition")
|
||||
scores["build123d"].add(
|
||||
18, "generalized experience can guide parametric feature composition"
|
||||
)
|
||||
scores["simplecadapi"].add(
|
||||
24, "generalized experience can bind to semantic roles and operation graphs"
|
||||
)
|
||||
|
||||
forced_backend = args.backend if args.backend != "auto" else None
|
||||
if (
|
||||
forced_backend
|
||||
and availability
|
||||
and scores[forced_backend].score < -5_000
|
||||
):
|
||||
raise ValueError(
|
||||
f"Explicit backend '{forced_backend}' is unavailable or incompatible "
|
||||
"in the probed runtime"
|
||||
)
|
||||
ranked = sorted(scores.values(), key=lambda item: (-item.score, item.name))
|
||||
if forced_backend:
|
||||
selected = scores[forced_backend]
|
||||
@@ -500,7 +685,7 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
profiles.extend(["requirement_refinement", "visual_repair"])
|
||||
if not any(char.isdigit() for char in request) and contains_any(
|
||||
text,
|
||||
KEYWORD_GROUPS["general_mechanical"] + KEYWORD_GROUPS["special_mechanical"],
|
||||
KEYWORD_GROUPS["general_mechanical"] + KEYWORD_GROUPS["standard_factory"],
|
||||
):
|
||||
profiles.append("requirement_refinement")
|
||||
profiles = list(dict.fromkeys(profiles))
|
||||
@@ -523,6 +708,29 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
"project": backend_info["project"],
|
||||
"adapter": backend_info.get("adapter"),
|
||||
"confidence": confidence,
|
||||
"routing_policy": {
|
||||
"version": "2.0",
|
||||
"default_backend": None,
|
||||
"selection_scope": (
|
||||
"post_reconstruction_semantic_backend"
|
||||
if teacher_step_reconstruction
|
||||
else "authoring_backend"
|
||||
),
|
||||
"requirements": requirements,
|
||||
"native_source_backend": source_backend,
|
||||
"score_margin": gap,
|
||||
"effective_execution_engine": (
|
||||
"surfaceir" if teacher_step_reconstruction else selected.name
|
||||
),
|
||||
"validation_engines": (
|
||||
[
|
||||
"surfaceir_geometry_acceptance",
|
||||
"simplecadapi_brep_compare",
|
||||
]
|
||||
if teacher_step_reconstruction
|
||||
else ["backend_geometry_validation"]
|
||||
),
|
||||
},
|
||||
"backend_scores": [
|
||||
{"backend": item.name, "score": item.score, "reasons": item.reasons}
|
||||
for item in sorted(scores.values(), key=lambda item: (-item.score, item.name))
|
||||
@@ -631,10 +839,10 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
),
|
||||
},
|
||||
"project_contributions": {
|
||||
"text-to-cad": "core Skill host, build123d CAD backend, refinement, visual repair, validation, artifacts, and CAD Viewer handoff",
|
||||
"SimpleCADAPI": "specialized mechanical backend with replayable graphs and semantic modeling",
|
||||
"text-to-cad": "backend-neutral orchestrator, DesignIR owner, SurfaceIR reconstruction, refinement, repair, validation, artifacts, and Viewer handoff",
|
||||
"SimpleCADAPI": "equal-priority OCP backend for standard factories, replayable graphs, semantic lineage, units, tolerances, translators, and B-Rep diagnostics",
|
||||
},
|
||||
"availability": probe_backends() if args.probe else None,
|
||||
"availability": availability,
|
||||
}
|
||||
if teacher_step_reconstruction:
|
||||
result["execution_adapter"] = {
|
||||
@@ -642,6 +850,7 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
"runtime": "DesignIR 3.0 / SurfaceIR",
|
||||
"automatic_geometry_acceptance": True,
|
||||
"automatic_parameter_acceptance": True,
|
||||
"supplemental_brep_acceptance": "SimpleCADAPI 2.0.2",
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -684,6 +893,8 @@ def execute_reconstruction(
|
||||
str(args.maximum_parameters),
|
||||
"--timeout-seconds",
|
||||
str(args.timeout_seconds),
|
||||
"--semantic-backend",
|
||||
str(result["selected_backend"]),
|
||||
]
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
@@ -703,13 +914,22 @@ def execute_reconstruction(
|
||||
|
||||
|
||||
def explain(result: dict[str, object]) -> str:
|
||||
policy = result.get("routing_policy", {})
|
||||
lines = [
|
||||
f"Selected backend: {result['selected_backend']}",
|
||||
f"Runner skill: ${result['runner_skill']}",
|
||||
f"Project: {result['project']}",
|
||||
f"Confidence: {result['confidence']}",
|
||||
"Effective execution engine: "
|
||||
f"{policy.get('effective_execution_engine', result['selected_backend'])}",
|
||||
f"Source of truth: {result['source_of_truth']}",
|
||||
]
|
||||
requirements = policy.get("requirements", [])
|
||||
if requirements:
|
||||
lines.append(f"Requirements: {', '.join(requirements)}")
|
||||
validators = policy.get("validation_engines", [])
|
||||
if validators:
|
||||
lines.append(f"Validation engines: {', '.join(validators)}")
|
||||
profiles = result["workflow_profiles"]
|
||||
if profiles:
|
||||
lines.append(f"Workflow profiles: {', '.join(profiles)}")
|
||||
|
||||
Reference in New Issue
Block a user