refactor: add DesignIR distillation pipeline
This commit is contained in:
@@ -12,10 +12,6 @@ from pathlib import Path
|
||||
|
||||
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
||||
REGISTRY_PATH = Path(__file__).with_name("capabilities.json")
|
||||
CADAM_RUNTIME_FILES = (
|
||||
Path("src/vendor/openscad-wasm/openscad.js"),
|
||||
Path("src/vendor/openscad-wasm/openscad.wasm"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -40,14 +36,9 @@ KEYWORD_GROUPS: dict[str, tuple[str, ...]] = {
|
||||
"lever", "摇臂", "shaft", "轴", "bushing", "轴套", "flange", "法兰",
|
||||
"housing", "壳体", "enclosure", "外壳", "fixture", "夹具", "plate", "安装板",
|
||||
),
|
||||
"printable": (
|
||||
"3d print", "3d-print", "打印", "printable", "vase", "花瓶", "knob", "旋钮",
|
||||
"container", "容器", "adapter", "适配器", "honeycomb", "蜂窝", "lattice", "晶格",
|
||||
"pattern", "阵列", "decorative", "装饰", "openscad", "scad", "bosl",
|
||||
),
|
||||
"replay": (
|
||||
"replay", "重放", "operation graph", "model json", "模型图", "semantic tag",
|
||||
"语义标签", "freecad", "fcstd",
|
||||
"语义标签",
|
||||
),
|
||||
"visual_complexity": (
|
||||
"image", "photo", "sketch", "图片", "照片", "草图", "复杂", "complex",
|
||||
@@ -56,16 +47,6 @@ KEYWORD_GROUPS: dict[str, tuple[str, ...]] = {
|
||||
}
|
||||
|
||||
|
||||
FOCUSED_SKILLS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("dxf", ("dxf", "laser cut", "激光切割", "waterjet", "水刀", "flat pattern", "展开")),
|
||||
("urdf", ("urdf", "robot description", "机器人描述")),
|
||||
("srdf", ("srdf", "moveit", "planning group", "规划组")),
|
||||
("sdf", ("sdformat", "gazebo", "simulation world", "仿真世界", ".sdf")),
|
||||
("gcode", ("g-code", "gcode", "slice", "切片")),
|
||||
("sendcutsend", ("sendcutsend",)),
|
||||
("bambu-labs", ("bambu", "拓竹")),
|
||||
)
|
||||
|
||||
EXPERIENCE_FAMILY_RULES: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
(
|
||||
"flanged_hub_adapter",
|
||||
@@ -184,15 +165,6 @@ def contains_any(text: str, terms: tuple[str, ...]) -> bool:
|
||||
return any(term in text for term in terms)
|
||||
|
||||
|
||||
def normalize_formats(formats: list[str]) -> list[str]:
|
||||
result: list[str] = []
|
||||
for value in formats:
|
||||
normalized = value.lower().lstrip(".")
|
||||
if normalized not in result:
|
||||
result.append(normalized)
|
||||
return result
|
||||
|
||||
|
||||
def infer_experience_query(request: str) -> tuple[str | None, list[str]]:
|
||||
text = request.lower()
|
||||
family = next(
|
||||
@@ -211,18 +183,6 @@ def infer_experience_query(request: str) -> tuple[str | None, list[str]]:
|
||||
return family, features
|
||||
|
||||
|
||||
def cadam_root() -> Path:
|
||||
configured = os.environ.get("CADAM_ROOT", "").strip()
|
||||
if configured:
|
||||
return Path(configured).expanduser().resolve()
|
||||
return (SKILL_ROOT.parents[2] / "CADAM").resolve()
|
||||
|
||||
|
||||
def cadam_runtime_available(root: Path | None = None) -> bool:
|
||||
candidate = root or cadam_root()
|
||||
return all((candidate / relative_path).is_file() for relative_path in CADAM_RUNTIME_FILES)
|
||||
|
||||
|
||||
def probe_backends() -> dict[str, dict[str, object]]:
|
||||
return {
|
||||
"build123d": {
|
||||
@@ -233,10 +193,6 @@ def probe_backends() -> dict[str, dict[str, object]]:
|
||||
"available": importlib.util.find_spec("simplecadapi") is not None,
|
||||
"probe": f"python={sys.executable}",
|
||||
},
|
||||
"cadam": {
|
||||
"available": cadam_runtime_available(),
|
||||
"probe": str(cadam_root()),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -265,46 +221,6 @@ MAJOR_EDIT_TERMS = (
|
||||
"major redesign",
|
||||
"rebuild the whole",
|
||||
)
|
||||
LOCAL_FEATURE_EDIT_TERMS = (
|
||||
"增加孔",
|
||||
"添加孔",
|
||||
"删除孔",
|
||||
"增加槽",
|
||||
"添加槽",
|
||||
"删除槽",
|
||||
"沉孔",
|
||||
"倒角",
|
||||
"圆角",
|
||||
"add hole",
|
||||
"remove hole",
|
||||
"add slot",
|
||||
"counterbore",
|
||||
"fillet",
|
||||
"chamfer",
|
||||
)
|
||||
PARAMETER_EDIT_TERMS = (
|
||||
"孔径",
|
||||
"直径",
|
||||
"厚度",
|
||||
"长度",
|
||||
"宽度",
|
||||
"高度",
|
||||
"间距",
|
||||
"齿数",
|
||||
"模数",
|
||||
"改为",
|
||||
"调整",
|
||||
"diameter",
|
||||
"thickness",
|
||||
"length",
|
||||
"width",
|
||||
"height",
|
||||
"spacing",
|
||||
"change",
|
||||
"resize",
|
||||
)
|
||||
|
||||
|
||||
def _find_forbidden_experience_key(value: object) -> str | None:
|
||||
if isinstance(value, dict):
|
||||
for key, child in value.items():
|
||||
@@ -325,7 +241,14 @@ def default_experience_library() -> Path | None:
|
||||
configured = os.environ.get("CAD_EXPERIENCE_LIBRARY", "").strip()
|
||||
if configured:
|
||||
return Path(configured).expanduser()
|
||||
workspace_candidate = SKILL_ROOT.parents[2] / "cad-experience-library" / "library.json"
|
||||
workspace_candidate = (
|
||||
SKILL_ROOT.parents[2]
|
||||
/ "designir-pipeline"
|
||||
/ "workspace"
|
||||
/ "promotion"
|
||||
/ "promoted"
|
||||
/ "library.json"
|
||||
)
|
||||
return workspace_candidate if workspace_candidate.is_file() else None
|
||||
|
||||
|
||||
@@ -338,40 +261,43 @@ def classify_edit_context(
|
||||
return None
|
||||
source = edit_source.expanduser().resolve() if edit_source else None
|
||||
if source is not None:
|
||||
parser_root = (
|
||||
SKILL_ROOT.parents[2] / "cad-experience-plugin" / "parser"
|
||||
).resolve()
|
||||
if source == parser_root or parser_root in source.parents:
|
||||
protected_roots = [
|
||||
(
|
||||
SKILL_ROOT.parents[2]
|
||||
/ "designir-pipeline"
|
||||
/ "workspace"
|
||||
/ "teacher"
|
||||
).resolve(),
|
||||
(
|
||||
SKILL_ROOT.parents[2]
|
||||
/ "designir-pipeline"
|
||||
/ "workspace"
|
||||
/ "acceptance"
|
||||
).resolve(),
|
||||
]
|
||||
if any(source == root or root in source.parents for root in protected_roots):
|
||||
raise ValueError(
|
||||
"cad-router cannot use cad-experience-plugin/parser input or "
|
||||
"output as an edit source; provide the original model or "
|
||||
"editable generator outside the distillation staging area"
|
||||
"cad-router reconstruction cannot read the teacher or acceptance "
|
||||
"vault; pass the ingestion job reference or an editable DesignIR"
|
||||
)
|
||||
suffix = source.suffix.lower() if source else ""
|
||||
text = request.lower()
|
||||
|
||||
if suffix == ".json" and source and source.name == "model-spec.json":
|
||||
mode = "model_spec_parameter_edit"
|
||||
reason = "A per-part model specification exists; update its named JSON parameters or modification features."
|
||||
elif suffix in {".py", ".scad", ".js", ".mjs"}:
|
||||
if suffix == ".json" and source and source.name.endswith(".designir.json"):
|
||||
mode = "designir_parameter_edit"
|
||||
reason = "Executable DesignIR exists; update its named parameters, constraints, or features."
|
||||
elif suffix == ".py":
|
||||
mode = "native_parameter_edit"
|
||||
reason = "An editable generator exists; update its named parameters or feature source."
|
||||
elif source and suffix in {".step", ".stp"}:
|
||||
mode = "private_evidence_to_isolated_designir_reconstruction"
|
||||
reason = (
|
||||
"The uploaded STEP is teacher truth: ingestion extracts private "
|
||||
"evidence, then reconstruction authors DesignIR without STEP access."
|
||||
)
|
||||
elif contains_any(text, MAJOR_EDIT_TERMS):
|
||||
mode = "experience_guided_semantic_reconstruction"
|
||||
reason = "The requested topology change is large; rebuild from the specified source model using only promoted experience."
|
||||
elif source and suffix in {".step", ".stp"} and contains_any(
|
||||
text, LOCAL_FEATURE_EDIT_TERMS
|
||||
):
|
||||
mode = "direct_step_local_feature_edit"
|
||||
reason = "Inspect the specified STEP and rebuild only the affected local feature."
|
||||
elif source and suffix in {".step", ".stp"} and contains_any(
|
||||
text, PARAMETER_EDIT_TERMS
|
||||
):
|
||||
mode = "direct_step_feature_rebuild"
|
||||
reason = "STEP has no native feature history; inspect the source and reconstruct the affected dimensional feature."
|
||||
elif source and suffix in {".step", ".stp"}:
|
||||
mode = "inspect_existing_step_then_modify"
|
||||
reason = "Inspect the explicitly supplied STEP before choosing local editing or reconstruction."
|
||||
else:
|
||||
mode = "existing_source_required"
|
||||
reason = "An edit was requested without a resolvable editable source or private case."
|
||||
@@ -381,21 +307,30 @@ def classify_edit_context(
|
||||
"edit_source": str(source) if source else None,
|
||||
"modification_mode": mode,
|
||||
"reason": reason,
|
||||
"allowed_knowledge_sources": [
|
||||
"the explicitly supplied original model or editable generator",
|
||||
"the promoted generalized CAD experience library",
|
||||
"the current user request",
|
||||
],
|
||||
"allowed_knowledge_sources": (
|
||||
[
|
||||
"deterministically extracted private teacher evidence",
|
||||
"the promoted generalized CAD experience library",
|
||||
"the current user request",
|
||||
]
|
||||
if source and suffix in {".step", ".stp"}
|
||||
else [
|
||||
"the explicitly supplied DesignIR or editable generator",
|
||||
"the promoted generalized CAD experience library",
|
||||
"the current user request",
|
||||
]
|
||||
),
|
||||
"forbidden_knowledge_sources": [
|
||||
"cad-experience-plugin/parser/input",
|
||||
"cad-experience-plugin/parser/output",
|
||||
"private case JSON",
|
||||
"teacher STEP during reconstruction",
|
||||
"acceptance vault",
|
||||
"embedded B-Rep or mesh geometry",
|
||||
"private cases unrelated to the active teacher evidence",
|
||||
],
|
||||
"preservation_contract": [
|
||||
"Preserve the source coordinate frame unless the request explicitly changes it.",
|
||||
"Preserve unmodified interfaces, datums, and feature relationships.",
|
||||
"Prefer the per-part model-spec.json and native generator over inferred STEP reconstruction when they exist.",
|
||||
"Use generalized experience only as method guidance, never as replacement source geometry.",
|
||||
"Prefer the per-part DesignIR and native generator when they exist.",
|
||||
"Use generalized experience only as method guidance, never as teacher geometry.",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -481,7 +416,6 @@ def load_experience_library(
|
||||
def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
request = args.request.strip()
|
||||
text = request.lower()
|
||||
outputs = normalize_formats(args.output)
|
||||
inferred_family, inferred_features = infer_experience_query(request)
|
||||
experience_family = args.experience_family or inferred_family
|
||||
experience_features = args.experience_feature or inferred_features
|
||||
@@ -490,6 +424,11 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
args.edit_source,
|
||||
args.existing_model,
|
||||
)
|
||||
teacher_step_reconstruction = bool(
|
||||
edit_context
|
||||
and edit_context.get("modification_mode")
|
||||
== "private_evidence_to_isolated_designir_reconstruction"
|
||||
)
|
||||
experience_context = load_experience_library(
|
||||
args.experience_library,
|
||||
experience_family,
|
||||
@@ -499,7 +438,6 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
scores = {
|
||||
"build123d": BackendScore("build123d", 35, ["+35 general STEP-first default"]),
|
||||
"simplecadapi": BackendScore("simplecadapi", 20, ["+20 specialist mechanical baseline"]),
|
||||
"cadam": BackendScore("cadam", 10, ["+10 CADAM printable CSG baseline"]),
|
||||
}
|
||||
|
||||
if contains_any(text, KEYWORD_GROUPS["special_mechanical"]):
|
||||
@@ -508,42 +446,18 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
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["printable"]):
|
||||
scores["cadam"].add(55, "printable, pattern-heavy, or SCAD-native request")
|
||||
if contains_any(text, KEYWORD_GROUPS["replay"]):
|
||||
scores["simplecadapi"].add(60, "replay, semantic graph, or FreeCAD requirement")
|
||||
scores["simplecadapi"].add(60, "replay or semantic graph requirement")
|
||||
|
||||
if edit_context:
|
||||
scores["build123d"].add(55, "existing model inspection or source modification")
|
||||
scores["cadam"].add(-50, "poor fit for imported editable STEP")
|
||||
if args.assembly:
|
||||
scores["build123d"].add(15, "source-level assembly workflow")
|
||||
scores["simplecadapi"].add(18, "mechanism and constraint workflow")
|
||||
scores["cadam"].add(-15, "weak fit for engineering assembly relationships")
|
||||
if args.browser_controls:
|
||||
scores["cadam"].add(45, "CADAM-style parameter controls requested")
|
||||
if args.manufacturing == "machining":
|
||||
scores["build123d"].add(25, "machining favors STEP-first B-Rep")
|
||||
scores["simplecadapi"].add(18, "OCP STEP geometry suits machining")
|
||||
scores["cadam"].add(-25, "mesh/CSG route is not preferred for machining")
|
||||
elif args.manufacturing == "printing":
|
||||
scores["cadam"].add(30, "printing favors fast parametric CSG")
|
||||
scores["build123d"].add(8, "can export validated printable meshes")
|
||||
elif args.manufacturing == "laser-cutting":
|
||||
scores["build123d"].add(12, "can own projected source geometry")
|
||||
|
||||
if any(fmt in {"step", "stp"} for fmt in outputs):
|
||||
scores["build123d"].add(35, "editable STEP requested")
|
||||
scores["simplecadapi"].add(30, "OCP STEP export requested")
|
||||
scores["cadam"].add(-70, "no native editable STEP output")
|
||||
if "fcstd" in outputs:
|
||||
scores["simplecadapi"].add(70, "FreeCAD output requested")
|
||||
scores["cadam"].add(-40, "FreeCAD output is unsupported")
|
||||
if "scad" in outputs:
|
||||
scores["cadam"].add(80, "SCAD source requested")
|
||||
if any(fmt in {"stl", "3mf"} for fmt in outputs):
|
||||
scores["cadam"].add(12, "print mesh requested")
|
||||
scores["build123d"].add(5, "secondary mesh export is available")
|
||||
scores["build123d"].add(35, "editable STEP is the fixed artifact contract")
|
||||
scores["simplecadapi"].add(
|
||||
30, "OCP STEP export satisfies the fixed artifact 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")
|
||||
@@ -568,22 +482,9 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
profiles.append("requirement_refinement")
|
||||
profiles = list(dict.fromkeys(profiles))
|
||||
|
||||
downstream = [name for name, terms in FOCUSED_SKILLS if contains_any(text, terms)]
|
||||
if args.manufacturing == "laser-cutting" and "dxf" not in downstream:
|
||||
downstream.append("dxf")
|
||||
if any(fmt in {"stl", "3mf", "gcode"} for fmt in outputs) and "cad-viewer" not in downstream:
|
||||
downstream.append("cad-viewer")
|
||||
elif selected.name in {"build123d", "simplecadapi"}:
|
||||
downstream.append("cad-viewer")
|
||||
|
||||
registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
||||
backend_info = registry["backends"][selected.name]
|
||||
source_of_truth = backend_info["source_of_truth"]
|
||||
if (
|
||||
edit_context
|
||||
and edit_context.get("modification_mode") == "model_spec_parameter_edit"
|
||||
):
|
||||
source_of_truth = "per-part model-spec.json with a backend generator"
|
||||
source_of_truth = "per-part DesignIR 2.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] = {
|
||||
@@ -600,8 +501,10 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
],
|
||||
"fallback_order": fallback,
|
||||
"workflow_profiles": profiles,
|
||||
"downstream_skills": downstream,
|
||||
"requested_outputs": outputs,
|
||||
"artifact_contract": {
|
||||
"primary_format": "step",
|
||||
"editable_contract": "DesignIR 2.0",
|
||||
},
|
||||
"source_of_truth": source_of_truth,
|
||||
"experience_context": experience_context,
|
||||
"design_plan": {
|
||||
@@ -664,27 +567,37 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
else None
|
||||
),
|
||||
"knowledge_boundary": (
|
||||
"Classify only from the explicitly supplied source model; "
|
||||
"never from parser input/output or private case JSON."
|
||||
"Classify from active-job private evidence only; the "
|
||||
"reconstruction agent never reads teacher STEP."
|
||||
),
|
||||
},
|
||||
"edit_context": edit_context,
|
||||
"execution_steps": (
|
||||
[
|
||||
"Load only the explicitly supplied source model or native generator.",
|
||||
"Inspect the source model to classify its family and feature roles when the request does not name them, then re-query promoted experience.",
|
||||
"Run deterministic ingestion in the protected teacher zone.",
|
||||
"Give Agent A only the active private evidence packet and promoted experience.",
|
||||
"Author complete executable DesignIR with uncertainty and missing-capability declarations.",
|
||||
"Run anti-cheat validation and compile in a sandbox without teacher STEP.",
|
||||
"Give Agent B the teacher, DesignIR, rebuilt STEP, and edit contract.",
|
||||
"Run geometry comparison and parameter perturbation acceptance.",
|
||||
"Submit sanitized feedback to deterministic promotion policy.",
|
||||
]
|
||||
if teacher_step_reconstruction
|
||||
else
|
||||
[
|
||||
"Load only the explicitly supplied DesignIR or native generator.",
|
||||
"Map requested changes to named parameters or affected semantic feature roles.",
|
||||
"Preserve unchanged interfaces and rebuild only the smallest responsible feature region.",
|
||||
"Use promoted generalized experience to guide methods; never read parser input/output.",
|
||||
"Use promoted generalized experience to guide methods.",
|
||||
"Use semantic reconstruction for topology changes that cannot preserve the prior feature structure.",
|
||||
"Validate changed dimensions plus invariants inherited from the previous model.",
|
||||
]
|
||||
if edit_context
|
||||
else [
|
||||
"Resolve explicit user dimensions and preserve them as authoritative parameters.",
|
||||
"Instantiate matched reconstruction grammars as symbolic model-spec.json parameters, datums, and canonical feature stages; never copy teacher dimensions.",
|
||||
"Instantiate matched reconstruction grammars as DesignIR parameters, datums, constraints, and canonical feature stages; never copy teacher geometry.",
|
||||
"Translate requested feature roles and matched generalized methods into backend operations.",
|
||||
"Generate the editable source of truth and STEP-first artifact when requested.",
|
||||
"Generate the editable source of truth and STEP artifact.",
|
||||
"Validate topology, dimensions, feature relationships, surface mix, and export integrity against the grammar validation roles.",
|
||||
]
|
||||
),
|
||||
@@ -692,7 +605,6 @@ 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",
|
||||
"CADAM": "OpenSCAD-WASM execution for printable and pattern-heavy SCAD models",
|
||||
},
|
||||
"availability": probe_backends() if args.probe else None,
|
||||
}
|
||||
@@ -710,9 +622,6 @@ def explain(result: dict[str, object]) -> str:
|
||||
profiles = result["workflow_profiles"]
|
||||
if profiles:
|
||||
lines.append(f"Workflow profiles: {', '.join(profiles)}")
|
||||
downstream = result["downstream_skills"]
|
||||
if downstream:
|
||||
lines.append(f"Downstream skills: {', '.join('$' + name for name in downstream)}")
|
||||
experience_context = result.get("experience_context")
|
||||
if experience_context:
|
||||
lines.append(
|
||||
@@ -734,25 +643,18 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument("request", help="Natural-language CAD request.")
|
||||
parser.add_argument(
|
||||
"--backend",
|
||||
choices=("auto", "build123d", "simplecadapi", "cadam"),
|
||||
choices=("auto", "build123d", "simplecadapi"),
|
||||
default="auto",
|
||||
help="Override automatic routing.",
|
||||
)
|
||||
parser.add_argument("--output", action="append", default=[], help="Required output format; repeatable.")
|
||||
parser.add_argument(
|
||||
"--manufacturing",
|
||||
choices=("unspecified", "machining", "printing", "laser-cutting", "concept"),
|
||||
default="unspecified",
|
||||
)
|
||||
parser.add_argument("--existing-model", action="store_true", help="The task modifies or inspects an existing model.")
|
||||
parser.add_argument(
|
||||
"--edit-source",
|
||||
type=Path,
|
||||
help="Existing model-spec.json, generator, or explicit STEP/STP to modify; private case JSON is forbidden.",
|
||||
help="Existing *.designir.json, generator, or explicit STEP/STP teacher to reconstruct; private corpus access is forbidden.",
|
||||
)
|
||||
parser.add_argument("--assembly", action="store_true", help="The requested result is an assembly.")
|
||||
parser.add_argument("--browser-controls", action="store_true", help="Interactive browser parameter controls are required.")
|
||||
parser.add_argument("--probe", action="store_true", help="Probe current-interpreter and OpenSCAD availability.")
|
||||
parser.add_argument("--probe", action="store_true", help="Probe current-interpreter backend availability.")
|
||||
parser.add_argument(
|
||||
"--experience-library",
|
||||
type=Path,
|
||||
|
||||
Reference in New Issue
Block a user