feat: integrate SimpleCADAPI 2.0.2 CAD workflows
This commit is contained in:
@@ -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