import json import subprocess import sys import textwrap from pathlib import Path from agent_core.config import DEFAULT_CADQUERY_OUTPUT_PATH, WORKDIR from agent_core.outputs import resolve_run_output, update_latest_link, workspace_relative, write_manifest from agent_core.utils import json_response CADQUERY_RUNNER = r""" import contextlib import io import json import sys import traceback from pathlib import Path stdout_buffer = io.StringIO() stderr_buffer = io.StringIO() def tail(text, limit=4000): if not text: return "" return text[-limit:] def fail(stage, exc=None, error_type=None, error=None): payload = { "ok": False, "stage": stage, "error_type": error_type or (type(exc).__name__ if exc else "Error"), "error": error or (str(exc) if exc else ""), "traceback_tail": tail(traceback.format_exc() if exc else ""), "stdout": tail(stdout_buffer.getvalue()), "stderr": tail(stderr_buffer.getvalue()), } print(json.dumps(payload, ensure_ascii=False)) try: request = json.loads(sys.stdin.read()) code = request["code"] output_path = Path(request["output_path"]) preview_path = Path(request["preview_path"]) except Exception as exc: fail("input", exc) sys.exit(0) try: import cadquery as cq except Exception as exc: fail("import", exc) sys.exit(0) try: compiled = compile(code, "", "exec") except Exception as exc: fail("syntax", exc) sys.exit(0) namespace = { "__name__": "__cadquery_user_code__", "cq": cq, } try: with contextlib.redirect_stdout(stdout_buffer), contextlib.redirect_stderr(stderr_buffer): exec(compiled, namespace) except Exception as exc: fail("execution", exc) sys.exit(0) if "result" not in namespace: fail("result", error_type="MissingResult", error="CadQuery code must assign the final model to variable 'result'.") sys.exit(0) result = namespace["result"] if result is None: fail("result", error_type="InvalidResult", error="'result' is None.") sys.exit(0) exportable_types = tuple( value for name in ("Workplane", "Shape", "Assembly", "Sketch") for value in [getattr(cq, name, None)] if isinstance(value, type) ) is_exportable = isinstance(result, exportable_types) if isinstance(result, (list, tuple)) and result: is_exportable = all(isinstance(item, exportable_types) for item in result) if not is_exportable: fail( "result", error_type="InvalidResultType", error=f"'result' must be a CadQuery object or a non-empty list/tuple of CadQuery objects, got {type(result).__name__}.", ) sys.exit(0) try: output_path.parent.mkdir(parents=True, exist_ok=True) cq.exporters.export(result, str(output_path)) except Exception as exc: fail("export", exc) sys.exit(0) preview_error = None try: preview_path.parent.mkdir(parents=True, exist_ok=True) cq.exporters.export(result, str(preview_path)) except Exception as exc: preview_error = str(exc) payload = { "ok": True, "stage": "done", "output_path": str(output_path), "preview_path": str(preview_path) if preview_path.exists() else None, "warning": f"Failed to export STL preview: {preview_error}" if preview_error else None, "stdout": tail(stdout_buffer.getvalue()), "stderr": tail(stderr_buffer.getvalue()), } print(json.dumps(payload, ensure_ascii=False)) """ def run_execute_cadquery(code: str, output_path: str = DEFAULT_CADQUERY_OUTPUT_PATH, prompt: str | None = None) -> str: try: if not code or not code.strip(): return json_response({ "ok": False, "stage": "input", "error_type": "EmptyCode", "error": "execute_cadquery requires non-empty CadQuery code.", "traceback_tail": "", "stdout": "", "stderr": "", }) run_dir, output, run_id = resolve_run_output(output_path) preview = run_dir / "preview.stl" output_root = run_dir.parent.parent except Exception as exc: return json_response({ "ok": False, "stage": "input", "error_type": type(exc).__name__, "error": str(exc), "traceback_tail": "", "stdout": "", "stderr": "", }) request = { "code": code, "output_path": str(output), "preview_path": str(preview), } try: process = subprocess.run( [sys.executable, "-c", CADQUERY_RUNNER], input=json.dumps(request, ensure_ascii=False), cwd=WORKDIR, capture_output=True, text=True, timeout=120, ) except subprocess.TimeoutExpired as exc: return json_response({ "ok": False, "stage": "execution", "error_type": "TimeoutExpired", "error": "CadQuery execution timed out after 120 seconds.", "traceback_tail": "", "stdout": exc.stdout[-4000:] if exc.stdout else "", "stderr": exc.stderr[-4000:] if exc.stderr else "", }) except Exception as exc: return json_response({ "ok": False, "stage": "subprocess", "error_type": type(exc).__name__, "error": str(exc), "traceback_tail": "", "stdout": "", "stderr": "", }) raw_output = process.stdout.strip() if not raw_output: return json_response({ "ok": False, "stage": "subprocess", "error_type": "NoRunnerOutput", "error": "CadQuery runner returned no JSON output.", "traceback_tail": "", "stdout": "", "stderr": process.stderr[-4000:], }) try: payload = json.loads(raw_output.splitlines()[-1]) except Exception as exc: return json_response({ "ok": False, "stage": "subprocess", "error_type": type(exc).__name__, "error": f"Failed to parse CadQuery runner output: {exc}", "traceback_tail": "", "stdout": raw_output[-4000:], "stderr": process.stderr[-4000:], }) if process.returncode != 0 and payload.get("ok") is not False: payload = { "ok": False, "stage": "subprocess", "error_type": "RunnerFailed", "error": f"CadQuery runner exited with code {process.returncode}.", "traceback_tail": "", "stdout": raw_output[-4000:], "stderr": process.stderr[-4000:], } if payload.get("ok") and payload.get("output_path"): exported_path = Path(payload["output_path"]) manifest_path = write_manifest( run_dir=run_dir, run_id=run_id, requested_output_path=output_path, output_path=exported_path, code=code, prompt=prompt, payload=payload, ) latest_warning = update_latest_link(output_root, run_dir) payload["run_id"] = run_id payload["run_dir"] = workspace_relative(run_dir) payload["output_path"] = workspace_relative(exported_path) if payload.get("preview_path"): payload["preview_path"] = workspace_relative(Path(payload["preview_path"])) payload["manifest_path"] = workspace_relative(manifest_path) payload["latest_path"] = workspace_relative(output_root / "latest") if latest_warning: payload["warning"] = latest_warning return json_response(payload) TOOL_SCHEMA = { "name": "execute_cadquery", "description": textwrap.dedent(""" Execute CadQuery Python code and export the result as a STEP file. The code must assign the final CadQuery model to a variable named result. cadquery is pre-imported as cq. Do NOT supply output_path in normal usage. The tool automatically writes files to the configured artifact directory under a unique run directory. Only set output_path if the user explicitly requests a different location. """).strip(), "input_schema": { "type": "object", "properties": { "code": { "type": "string", "description": "CadQuery Python code. It must assign the final model to variable 'result'.", }, "output_path": { "type": "string", "description": "Advanced: custom STEP file path or output directory. Omit this field to use the configured artifact directory (recommended).", }, }, "required": ["code"], }, }