#!/usr/bin/env python3 from __future__ import annotations 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 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 class BackendScore: name: str score: int reasons: list[str] = field(default_factory=list) def add(self, points: int, reason: str) -> None: self.score += points self.reasons.append(f"{points:+d} {reason}") KEYWORD_GROUPS: dict[str, tuple[str, ...]] = { "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", "安装板", ), "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", "像", "参考图", "reference", ), } EXPERIENCE_FAMILY_RULES: tuple[tuple[str, tuple[str, ...]], ...] = ( ( "flanged_hub_adapter", ("flanged hub", "法兰轮毂", "法兰轴套", "法兰适配器", "带法兰轴套"), ), ("connecting_rod", ("connecting rod", "连杆")), ("gear", ("gear", "齿轮")), ("shaft", ("shaft", "轴类", "传动轴")), ("flange", ("flange", "法兰")), ("bushing", ("bushing", "sleeve", "轴套", "衬套")), ("plate", ("plate", "mounting plate", "安装板", "平板")), ("bracket", ("bracket", "mounting bracket", "支架", "托架")), ("block", ("block", "rectangular body", "方块", "块体")), ("manifold", ("manifold", "valve body", "housing", "歧管", "阀体", "壳体")), ("rotational_part", ("roller", "pulley", "cylindrical body", "滚轮", "带轮")), ) EXPERIENCE_FEATURE_RULES: tuple[tuple[str, tuple[str, ...]], ...] = ( ("base_flange", ("flange", "法兰", "底盘")), ("hollow_sleeve", ("hub", "sleeve", "bushing", "轴套", "衬套", "轮毂")), ("central_passage", ("through bore", "center bore", "通孔", "中心孔", "内孔")), ("counterbore", ("counterbore", "沉孔", "台阶孔")), ( "repeated_axial_hole_pattern", ("bolt circle", "hole pattern", "螺丝孔", "螺栓孔", "孔阵列"), ), ("conical_transition", ("cone", "taper", "锥", "斜面过渡")), ("gear_teeth", ("gear", "teeth", "齿轮", "齿")), ("coaxial_cylindrical_stack", ("stepped shaft", "阶梯轴", "同轴", "轴肩")), ( "repeated_axial_hole_pattern", ( "mounting holes", "corner holes", "four holes", "multiple holes", "bolt holes", "安装孔", "四个孔", "多个孔", ), ), ("planar_dominant_body", ("plate", "bracket", "flat body", "板", "支架")), ( "multi_level_planar_profile", ("pocket", "recess", "stepped plate", "台阶面", "凹槽", "沉台"), ), ( "rotational_body", ("cylindrical body", "roller", "pulley", "revolved", "圆柱体", "滚轮", "回转体"), ), ( "stepped_rotational_profile", ("stepped shaft", "multiple diameters", "shoulder", "阶梯轴", "多段直径", "轴肩"), ), ("shaft_like_body", ("shaft", "axle", "rod", "轴", "长杆")), ( "flange_like_rotational_body", ("flange", "flanged", "法兰", "带法兰"), ), ( "cylindrical_feature_network", ("bore", "passage", "drilled holes", "孔系", "流道", "钻孔"), ), ( "multi_axis_passage", ("cross hole", "intersecting passage", "multi-axis", "交叉孔", "多轴孔"), ), ) # The router vocabulary is request-facing (plate, bracket, flange), while the # distilled library vocabulary is geometry-facing (patterned_plate, # plate_or_bracket, flanged_rotational_part). Keep the query semantic by # expanding a request family into compatible generalized scopes; this never # uses held-out case geometry or instance parameters. EXPERIENCE_FAMILY_SCOPE_ALIASES: dict[str, set[str]] = { "flanged_hub_adapter": { "flanged_hub_adapter", "flanged_rotational_part", "hollow_or_stepped_shaft", "rotational_part", }, "connecting_rod": { "connecting_rod", "patterned_prismatic_part", "plate_or_bracket", }, "gear": {"gear", "rotational_part"}, "shaft": {"shaft", "hollow_or_stepped_shaft", "rotational_part"}, "flange": {"flange", "flanged_rotational_part", "rotational_part"}, "bushing": {"bushing", "hollow_or_stepped_shaft", "rotational_part"}, "plate": {"plate", "patterned_plate", "plate_or_bracket"}, "bracket": { "bracket", "plate_or_bracket", "patterned_plate", "patterned_prismatic_part", }, "block": {"block", "prismatic_block", "patterned_prismatic_part"}, "manifold": { "manifold", "multi_axis_manifold", "prismatic_block", "patterned_prismatic_part", }, "rotational_part": { "rotational_part", "shaft", "hollow_or_stepped_shaft", "flanged_rotational_part", }, } def contains_any(text: str, terms: tuple[str, ...]) -> bool: return any(term in text for term in terms) def simplecadapi_standard_part_match( request: str, registry: dict[str, object], ) -> dict[str, object] | None: """Match only a requested *primary* standard part, never a feature word. The catalog is maintained beside the router from the actual SimpleCADAPI ``simplecadapi.std`` factories. This is intentionally deterministic: an unmatched or ambiguous request is a build123d request. """ catalog = registry.get("routing_catalog", {}) rules = ( catalog.get("simplecadapi_standard_part_types", []) if isinstance(catalog, dict) else [] ) text = request.casefold() best_match: tuple[int, dict[str, object], list[str]] | None = None for raw_rule in rules: if not isinstance(raw_rule, dict): continue aliases = [str(value).casefold() for value in raw_rule.get("aliases", [])] exclusions = [str(value).casefold() for value in raw_rule.get("feature_context_exclusions", [])] matched_aliases = [alias for alias in aliases if alias and alias in text] if not matched_aliases: continue if any(exclusion and exclusion in text for exclusion in exclusions): continue # Generic terms such as "gear" must never hide a more specific # standard-part type such as "herringbone gear". specificity = max(len(alias) for alias in matched_aliases) if best_match is None or specificity > best_match[0]: best_match = (specificity, raw_rule, matched_aliases) if best_match is None: return None _, rule, matched_aliases = best_match return { "type": rule.get("type"), "factory": rule.get("factory"), "matched_aliases": matched_aliases, } def infer_experience_query(request: str) -> tuple[str | None, list[str]]: text = request.lower() family = next( ( name for name, terms in EXPERIENCE_FAMILY_RULES if contains_any(text, terms) ), None, ) features = [ name for name, terms in EXPERIENCE_FEATURE_RULES if contains_any(text, terms) ] return family, features 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", "coordinates", "coordinate", "center", "location", "axis_origin", "source_sha256", "source_path", "face_refs", "surface_ids", "evidence", } MAJOR_EDIT_TERMS = ( "整体重做", "完全重做", "重新设计", "改成另一种", "大改", "拓扑重建", "major redesign", "rebuild the whole", ) def _find_forbidden_experience_key(value: object) -> str | None: if isinstance(value, dict): for key, child in value.items(): if key in EXPERIENCE_FORBIDDEN_KEYS: return key found = _find_forbidden_experience_key(child) if found: return found elif isinstance(value, list): for child in value: found = _find_forbidden_experience_key(child) if found: return found return None def default_experience_library() -> Path | None: configured = os.environ.get("CAD_EXPERIENCE_LIBRARY", "").strip() if configured: return Path(configured).expanduser() published_candidate = ( SKILL_ROOT.parents[2] / "designir-pipeline" / "output" / "library.json" ) return published_candidate if published_candidate.is_file() else None def classify_edit_context( request: str, edit_source: Path | None, existing_model: bool, ) -> dict[str, object] | None: if not (edit_source or existing_model): return None source = edit_source.expanduser().resolve() if edit_source else None if source is not None: protected_roots = [ ( SKILL_ROOT.parents[2] / "designir-pipeline" / "input" ).resolve(), ( SKILL_ROOT.parents[2] / "designir-pipeline" / "runs" ).resolve(), ] if any(source == root or root in source.parents for root in protected_roots): raise ValueError( "cad-router reconstruction cannot read distillation input or run " "artifacts; pass an editable DesignIR outside the private pipeline" ) suffix = source.suffix.lower() if source else "" text = request.lower() 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." else: mode = "existing_source_required" reason = "An edit was requested without a resolvable editable source or private case." return { "operation": "modify", "edit_source": str(source) if source else None, "modification_mode": mode, "reason": reason, "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": [ "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 DesignIR and native generator when they exist.", "Use generalized experience only as method guidance, never as teacher geometry.", ], } 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 resolved = candidate.expanduser().resolve() try: payload = json.loads(resolved.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: raise ValueError(f"Cannot read generalized CAD experience library: {resolved}: {exc}") from exc if payload.get("schema_version") != "2.0": raise ValueError(f"Unsupported generalized CAD experience schema in {resolved}") if payload.get("library_kind") != "generalized_cad_experience": raise ValueError(f"Not a generalized CAD experience library: {resolved}") policy = payload.get("policy") if not isinstance(policy, dict): raise ValueError(f"CAD experience library has no policy: {resolved}") if ( policy.get("instance_parameters_allowed") is not False or policy.get("absolute_coordinates_allowed") is not False or policy.get("single_case_promotion_allowed") is not False ): raise ValueError("CAD experience library policy permits instance-answer leakage") 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}") requested_features = { item.strip().lower().replace(" ", "_") for item in (features or []) if item.strip() } normalized_family = family.strip().lower().replace(" ", "_") if family else None compatible_scopes = ( EXPERIENCE_FAMILY_SCOPE_ALIASES.get( normalized_family, {normalized_family} ) if normalized_family else set() ) selected: list[object] = [] for item in payload.get("experiences", []): 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 required = set(item.get("when", {}).get("features", [])) if required and not required.issubset(requested_features): continue selected.append(item) return { "schema_version": "2.0", "context_kind": "generalized_cad_experience_query", "family": normalized_family, "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, }, } def route(args: argparse.Namespace) -> dict[str, object]: request = args.request.strip() text = request.lower() inferred_family, inferred_features = infer_experience_query(request) experience_family = args.experience_family or inferred_family experience_features = args.experience_feature or inferred_features edit_context = classify_edit_context( request, 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, experience_features, "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, ) if not edit_context and args.backend == "auto": registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8")) standard_part = simplecadapi_standard_part_match(request, registry) if not standard_part: requirements = [ requirement for requirement in requirements if requirement != "standard_mechanical_factory" ] agent_experiences = ( experience_context.get("experiences", []) if isinstance(experience_context, dict) else [] ) agent_generalized_methods = [ { key: item[key] for key in ( "id", "kind", "guidance", "check", "repair", "confidence", "reconstruction_grammar", ) if isinstance(item, dict) and key in item } for item in agent_experiences if isinstance(item, dict) ] agent_reconstruction_grammars = [ item["reconstruction_grammar"] for item in agent_experiences if isinstance(item, dict) and item.get("kind") == "reconstruction_grammar" and "reconstruction_grammar" in item ] selected_name = "simplecadapi" if standard_part else "build123d" backend_info = registry["backends"][selected_name] source_of_truth = ( "simplecadapi model graph with native Python source" if selected_name == "simplecadapi" else "build123d native Python source" ) selection_reason = ( "matched SimpleCADAPI standard-part catalog entry " f"{standard_part['type']} ({standard_part['factory']})" if standard_part else "no SimpleCADAPI standard-part catalog entry matched; use the general text-to-cad/build123d backend" ) return { "schema_version": "1.0", "request": request, "selected_backend": selected_name, "runner_skill": backend_info["runner_skill"], "project": backend_info["project"], "adapter": None, "confidence": "high", "routing_policy": { "version": "4.0", "selection_scope": "new_model_primary_part_type", "requirements": requirements, "decision_authority": "cad_router_standard_part_catalog", "keyword_backend_scoring": True, "fallback_policy": "unmatched_requests_use_build123d", "standard_part_match": standard_part, "server_side_template_generation": False, "effective_execution_engine": selected_name, "validation_engines": ["backend_geometry_validation"], }, "backend_scores": [ { "backend": selected_name, "score": 100, "reasons": [selection_reason], }, { "backend": "build123d" if selected_name == "simplecadapi" else "simplecadapi", "score": 0, "reasons": ["not selected by the standard-part catalog"], }, ], "fallback_order": ["build123d"] if selected_name == "simplecadapi" else [], "workflow_profiles": [], "artifact_contract": { "primary_format": "step", "editable_contract": "DesignIR 3.0 normalized contract", "editable_contract_role": "contract_after_backend_generation", "authoritative_geometry_source": source_of_truth, }, "source_of_truth": source_of_truth, "experience_context": experience_context, "design_plan": { "plan_kind": "deterministic_standard_part_route", "operation": "create", "part_family": experience_family or "unclassified", "requested_feature_roles": sorted(set(experience_features)), "selected_backend": selected_name, "source_of_truth": source_of_truth, "standard_part_match": standard_part, "generalized_methods": agent_generalized_methods, "reconstruction_grammars": agent_reconstruction_grammars, "experience_status": ( "matched" if experience_context and experience_context.get("experiences") else "no_promoted_method_matched" ), "execution_steps": [ "CAD Router selects the backend from the standard-part catalog.", "Agent writes backend-native Python source for the full request.", "Server executes the submitted source and rejects missing artifacts.", "Server normalizes backend output into DesignIR 3.0.", ], }, "project_contributions": { "text-to-cad": "default backend for every request outside the SimpleCADAPI standard-part catalog", "SimpleCADAPI": "backend for the catalogued standard mechanical part types only", }, "availability": None, } scores = { "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["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( 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 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(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( 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( 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] selected.reasons.append("explicit backend override") fallback = [item.name for item in ranked if item.name != forced_backend] else: selected = ranked[0] fallback = [item.name for item in ranked[1:]] profiles: list[str] = [] if contains_any(text, KEYWORD_GROUPS["visual_complexity"]): 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["standard_factory"], ): profiles.append("requirement_refinement") profiles = list(dict.fromkeys(profiles)) registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8")) backend_info = registry["backends"][selected.name] editable_contract = "DesignIR 3.0 normalized contract" if teacher_step_reconstruction: source_of_truth = "surfaceir_designir" elif selected.name == "simplecadapi": source_of_truth = "simplecadapi model graph with native Python source" else: source_of_truth = "build123d native Python source" 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] = { "schema_version": "1.0", "request": request, "selected_backend": selected.name, "runner_skill": backend_info["runner_skill"], "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)) ], "fallback_order": fallback, "workflow_profiles": profiles, "artifact_contract": { "primary_format": "step", "editable_contract": editable_contract, "editable_contract_role": "contract_after_backend_generation", "authoritative_geometry_source": source_of_truth, }, "source_of_truth": source_of_truth, "experience_context": experience_context, "design_plan": { "plan_kind": "backend_neutral_parametric_design_plan", "operation": "modify" if edit_context else "create", "part_family": experience_family or "unclassified", "requested_feature_roles": sorted(set(experience_features)), "selected_backend": selected.name, "source_of_truth": source_of_truth, "generalized_methods": [ { key: item[key] for key in ( "id", "kind", "guidance", "check", "repair", "confidence", "reconstruction_grammar", ) if key in item } for item in ( experience_context.get("experiences", []) if experience_context else [] ) if isinstance(item, dict) ], "reconstruction_grammars": [ item["reconstruction_grammar"] for item in ( experience_context.get("experiences", []) if experience_context else [] ) if isinstance(item, dict) and item.get("kind") == "reconstruction_grammar" and isinstance(item.get("reconstruction_grammar"), dict) ], "experience_status": ( "pending_source_classification" if needs_source_classification else ( "matched" if experience_context and experience_context.get("experiences") else "no_promoted_method_matched" ) ), "experience_query_plan": { "requires_source_inspection": needs_source_classification, "family": experience_family, "feature_roles": sorted(set(experience_features)), "retry_after_inspection": needs_source_classification, "retry_arguments": ( "--experience-family " "--experience-feature " if needs_source_classification else None ), "knowledge_boundary": ( "Classify from active-job private evidence only; the " "reconstruction agent never reads teacher STEP." ), }, "edit_context": edit_context, "execution_steps": ( [ "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.", "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.", "Translate requested feature roles and matched generalized methods into the selected backend's native operations.", "Generate backend-native source or model graph and export the primary STEP artifact.", "Normalize backend metadata into DesignIR 3.0 for parameters, feature tree, validation, and future edit bindings.", "Validate topology, dimensions, feature relationships, surface mix, and export integrity against the brief.", ] ), }, "project_contributions": { "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": availability, } if teacher_step_reconstruction: result["execution_adapter"] = { "path": str(RECONSTRUCTION_ADAPTER), "runtime": "DesignIR 3.0 / SurfaceIR", "automatic_geometry_acceptance": True, "automatic_parameter_acceptance": True, "supplemental_brep_acceptance": "SimpleCADAPI 2.0.2", } 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), "--semantic-backend", str(result["selected_backend"]), ] 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: 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)}") experience_context = result.get("experience_context") if experience_context: lines.append( "Experience: " f"{experience_context.get('family') or 'global'} " f"({len(experience_context.get('experiences', []))} generalized methods, " "no case parameters or coordinates)" ) lines.append("Ranking:") for item in result["backend_scores"]: lines.append(f" {item['backend']}: {item['score']}") for reason in item["reasons"]: lines.append(f" - {reason}") return "\n".join(lines) def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Route a CAD request to a V1 modeling backend.") parser.add_argument("request", help="Natural-language CAD request.") parser.add_argument( "--backend", choices=("auto", "build123d", "simplecadapi"), default="auto", help="Override automatic routing.", ) 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 *.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("--probe", action="store_true", help="Probe current-interpreter backend availability.") parser.add_argument( "--experience-library", type=Path, help="Schema 2.0 generalized CAD experience library; case JSON is rejected.", ) parser.add_argument("--experience-family", help="Optional generalized experience family filter.") parser.add_argument( "--experience-feature", action="append", default=[], help="Requested semantic feature role; repeatable.", ) 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) args.manifest.write_text(payload, encoding="utf-8") print(explain(result) if args.explain else payload, end="\n" if args.explain else "") return 0 if __name__ == "__main__": raise SystemExit(main())