""" Safe CadQuery code execution engine. Executes LLM-generated CadQuery code in a sandboxed namespace, validates the result, and exports to STEP/STL. """ import io import sys import traceback from pathlib import Path from typing import Any, Optional import cadquery as cq from pydantic import BaseModel, Field class ExecutionResult(BaseModel): """Result of executing a CadQuery script.""" model_config = {"arbitrary_types_allowed": True} success: bool result: Optional[Any] = Field(default=None, exclude=True) # cq.Workplane code: str = Field(default="", exclude=True) error: Optional[str] = None stdout: str = Field(default="", exclude=True) volume: float = Field(default=0.0, serialization_alias="volume_mm3") bounding_box: tuple = Field(default=(), serialization_alias="bounding_box_mm") face_count: int = 0 edge_count: int = 0 def summary(self) -> str: if not self.success: return f"FAILED: {self.error}" bb = self.bounding_box return ( f"OK | Volume: {self.volume:.1f} mm³ | " f"BBox: {bb[0]:.1f}×{bb[1]:.1f}×{bb[2]:.1f} mm | " f"Faces: {self.face_count} | Edges: {self.edge_count}" ) # Allowed imports in the sandboxed namespace SAFE_NAMESPACE = { "cq": cq, "cadquery": cq, "math": __import__("math"), "__builtins__": { "range": range, "len": len, "abs": abs, "min": min, "max": max, "round": round, "int": int, "float": float, "tuple": tuple, "list": list, "True": True, "False": False, "None": None, "print": print, "enumerate": enumerate, "zip": zip, }, } def sanitize_code(code: str) -> str: """Clean up LLM output — strip markdown fences, trailing whitespace, and redundant import statements (already in namespace).""" code = code.strip() # Remove markdown code fences if present if code.startswith("```python"): code = code[len("```python"):] elif code.startswith("```"): code = code[3:] if code.endswith("```"): code = code[:-3] # Strip import lines for modules already in namespace lines = code.strip().splitlines() cleaned = [] for line in lines: stripped = line.strip() # Keep the line unless it's a redundant cadquery/math import if stripped.startswith("import cadquery") or stripped.startswith("from cadquery"): continue if stripped == "import math": continue cleaned.append(line) return "\n".join(cleaned).strip() def execute_cadquery(code: str) -> ExecutionResult: """ Execute a CadQuery script string and return the result. The script must assign its output to a variable called `result`. """ code = sanitize_code(code) # Capture stdout old_stdout = sys.stdout sys.stdout = captured = io.StringIO() namespace = dict(SAFE_NAMESPACE) try: exec(code, namespace) # noqa: S102 except Exception: sys.stdout = old_stdout return ExecutionResult( success=False, code=code, error=traceback.format_exc(), stdout=captured.getvalue(), ) sys.stdout = old_stdout stdout_text = captured.getvalue() # Extract the result result_obj = namespace.get("result") if result_obj is None: return ExecutionResult( success=False, code=code, error="Script did not assign a value to `result`.", stdout=stdout_text, ) if not isinstance(result_obj, cq.Workplane): return ExecutionResult( success=False, code=code, error=f"Expected cq.Workplane, got {type(result_obj).__name__}", stdout=stdout_text, ) # Extract geometry metadata try: shape = result_obj.val() bb = result_obj.val().BoundingBox() bbox_dims = (bb.xlen, bb.ylen, bb.zlen) volume = shape.Volume() faces = len(result_obj.faces().vals()) edges = len(result_obj.edges().vals()) except Exception as e: return ExecutionResult( success=False, code=code, error=f"Geometry extraction failed: {e}", stdout=stdout_text, ) return ExecutionResult( success=True, result=result_obj, code=code, stdout=stdout_text, volume=volume, bounding_box=bbox_dims, face_count=faces, edge_count=edges, ) def export_step(result: cq.Workplane, path: str | Path) -> Path: """Export a CadQuery workplane to STEP format.""" path = Path(path) cq.exporters.export(result, str(path), exportType="STEP") return path def export_stl(result: cq.Workplane, path: str | Path, tolerance: float = 0.01) -> Path: """Export a CadQuery workplane to STL format.""" path = Path(path) cq.exporters.export(result, str(path), exportType="STL", tolerance=tolerance) return path def export_3mf(result: cq.Workplane, path: str | Path) -> Path: """Export a CadQuery workplane to 3MF format (slicer-ready).""" path = Path(path) cq.exporters.export(result, str(path), exportType="3MF") return path def export_all(result: cq.Workplane, base_path: str | Path) -> dict[str, Path]: """Export to STEP, STL, and 3MF.""" base = Path(base_path) return { "step": export_step(result, base.with_suffix(".step")), "stl": export_stl(result, base.with_suffix(".stl")), "3mf": export_3mf(result, base.with_suffix(".3mf")), }