From 4f1120bbdb9ee292412d50c4d222eebf1cbd0982 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:30:45 +0000 Subject: [PATCH 1/3] Initial plan From 98398720b1e8806d3e5af3956c93975d41d816f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:34:30 +0000 Subject: [PATCH 2/3] Add cube/box dimension parsing and generation to planner Co-authored-by: deanhu0822 <86739329+deanhu0822@users.noreply.github.com> --- opencad_agent/planner.py | 102 ++++++++++++++++++++++++++++++ opencad_agent/tests/test_agent.py | 87 ++++++++++++++++++++++++- 2 files changed, 188 insertions(+), 1 deletion(-) diff --git a/opencad_agent/planner.py b/opencad_agent/planner.py index f571c4c..ae40bbd 100644 --- a/opencad_agent/planner.py +++ b/opencad_agent/planner.py @@ -1,11 +1,45 @@ from __future__ import annotations +import re from typing import Any, Callable from opencad_agent.models import OperationExecution from opencad_agent.tools import ToolRuntime +def _parse_cube_dimensions(message: str) -> tuple[float, float, float] | None: + """Extract cube/box dimensions from a user message. + + Supports formats like: + - "30x30x30 mm" or "30x30x30mm" + - "30 mm cube" or "30mm cube" + - "cube of 30 mm" or "cube of 30mm" + - "box 10x20x30" + + Returns (length, width, height) or None if not found. + """ + lowered = message.lower() + + # Pattern: NxNxN (e.g., "30x30x30 mm") + match = re.search(r"(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)", lowered) + if match: + return float(match.group(1)), float(match.group(2)), float(match.group(3)) + + # Pattern: N mm cube or Nmm cube (uniform cube) + match = re.search(r"(\d+(?:\.\d+)?)\s*(?:mm)?\s*cube", lowered) + if match: + size = float(match.group(1)) + return size, size, size + + # Pattern: cube of N mm or cube of Nmm + match = re.search(r"cube\s+of\s+(\d+(?:\.\d+)?)", lowered) + if match: + size = float(match.group(1)) + return size, size, size + + return None + + class OpenCadPlanner: def execute(self, message: str, runtime: ToolRuntime, reasoning: bool = False) -> tuple[str, list[OperationExecution]]: lowered = message.lower() @@ -21,6 +55,18 @@ def execute(self, message: str, runtime: ToolRuntime, reasoning: bool = False) - response = "Mounting bracket feature sequence generated and executed." return response, operations + # Handle cube/box requests with dimension parsing + if "cube" in lowered or "box" in lowered: + dims = _parse_cube_dimensions(message) + if dims: + length, width, height = dims + operations = self._build_box(runtime, length, width, height) + if reasoning: + response = f"Plan: create {length}x{width}x{height} box via sketch and extrude. Sequence executed." + else: + response = f"Created a {length}x{width}x{height} box." + return response, operations + operations = self._build_simple_feature(runtime) response = "Executed a minimal sketch-to-extrude sequence for the request." if reasoning: @@ -43,6 +89,43 @@ def _safe_call( operations.append(OperationExecution(tool=tool, status="error", arguments=arguments, result=error)) raise + def _build_box(self, runtime: ToolRuntime, length: float, width: float, height: float) -> list[OperationExecution]: + """Build a box/cube with the specified dimensions via sketch + extrude.""" + operations: list[OperationExecution] = [] + + box_sketch_args = { + "name": "Box Base Sketch", + "entities": { + "l1": {"id": "l1", "type": "line", "start": (0.0, 0.0), "end": (length, 0.0)}, + "l2": {"id": "l2", "type": "line", "start": (length, 0.0), "end": (length, width)}, + "l3": {"id": "l3", "type": "line", "start": (length, width), "end": (0.0, width)}, + "l4": {"id": "l4", "type": "line", "start": (0.0, width), "end": (0.0, 0.0)}, + }, + "profile_order": ["l1", "l2", "l3", "l4"], + "constraints": [ + {"id": "d1", "type": "distance", "a": "l1", "value": length}, + {"id": "d2", "type": "distance", "a": "l2", "value": width}, + ], + } + + sketch = self._safe_call( + operations, + "add_sketch", + box_sketch_args, + lambda: {"sketch_id": runtime.add_sketch(**box_sketch_args)}, + ) + sketch_id = str(sketch["sketch_id"]) + + extrude_args = {"sketch_id": sketch_id, "depth": height, "name": "Box Extrude"} + self._safe_call( + operations, + "extrude", + extrude_args, + lambda: {"feature_id": runtime.extrude(**extrude_args)}, + ) + + return operations + def _build_simple_feature(self, runtime: ToolRuntime) -> list[OperationExecution]: operations: list[OperationExecution] = [] @@ -256,6 +339,25 @@ def generate_code(self, message: str) -> str: ').offset(0.4, name="Carrier Reinforcement")\n' ) + # Handle cube/box requests + if "cube" in lowered or "box" in lowered: + dims = _parse_cube_dimensions(message) + if dims: + length, width, height = dims + # Check if it's a uniform cube + if length == width == height: + return ( + f'"""Generated OpenCAD example: {length}mm cube."""\n\n' + "from opencad import Part\n\n\n" + f'Part(name="Generated Cube").box({length}, {length}, {length}, name="Cube")\n' + ) + else: + return ( + f'"""Generated OpenCAD example: {length}x{width}x{height} box."""\n\n' + "from opencad import Part\n\n\n" + f'Part(name="Generated Box").box({length}, {width}, {height}, name="Box")\n' + ) + return ( '"""Generated OpenCAD example: simple extruded part."""\n\n' "from opencad import Part, Sketch\n\n\n" diff --git a/opencad_agent/tests/test_agent.py b/opencad_agent/tests/test_agent.py index c1c6612..2aa6419 100644 --- a/opencad_agent/tests/test_agent.py +++ b/opencad_agent/tests/test_agent.py @@ -9,7 +9,7 @@ from opencad_agent.api import app from opencad_agent.llm import LiteLlmProvider from opencad_agent.models import ChatRequest -from opencad_agent.planner import OpenCadPlanner +from opencad_agent.planner import OpenCadPlanner, _parse_cube_dimensions from opencad_agent.prompting import build_code_generation_prompt, build_system_prompt from opencad_agent.service import OpenCadAgentService from opencad_agent.tools import ToolRuntime @@ -275,3 +275,88 @@ def test_agent_examples_readme_includes_claude_and_gemini_commands() -> None: assert "OPENCAD_LLM_MODEL=claude-3-5-sonnet-latest" in readme assert "OPENCAD_LLM_PROVIDER=gemini" in readme assert "OPENCAD_LLM_MODEL=gemini-2.0-flash" in readme + + +# ── Cube/Box dimension parsing tests ──────────────────────────────────── + + +@pytest.mark.parametrize( + ("message", "expected"), + [ + ("build a cube of 30x30x30 mm", (30.0, 30.0, 30.0)), + ("create a 30x30x30mm cube", (30.0, 30.0, 30.0)), + ("box 10x20x30", (10.0, 20.0, 30.0)), + ("30 mm cube", (30.0, 30.0, 30.0)), + ("30mm cube", (30.0, 30.0, 30.0)), + ("cube of 25", (25.0, 25.0, 25.0)), + ("15.5 x 20.5 x 30.5 box", (15.5, 20.5, 30.5)), + ], +) +def test_parse_cube_dimensions(message: str, expected: tuple[float, float, float]) -> None: + assert _parse_cube_dimensions(message) == expected + + +def test_parse_cube_dimensions_returns_none_for_invalid() -> None: + assert _parse_cube_dimensions("create a sphere") is None + assert _parse_cube_dimensions("make something") is None + + +@pytest.mark.parametrize( + ("message", "expected_in_code"), + [ + ("build a cube of 30x30x30 mm", '.box(30.0, 30.0, 30.0, name="Cube")'), + ("create a 10x20x30 box", '.box(10.0, 20.0, 30.0, name="Box")'), + ("make a 25mm cube", '.box(25.0, 25.0, 25.0, name="Cube")'), + ], +) +def test_planner_generate_code_for_cubes_and_boxes(message: str, expected_in_code: str) -> None: + code = OpenCadPlanner().generate_code(message) + assert "from opencad import Part" in code + assert expected_in_code in code + + +def test_planner_execute_handles_cube_request() -> None: + service = OpenCadAgentService() + request = ChatRequest( + message="build a cube of 30x30x30 mm", + tree_state=_seed_tree(), + conversation_history=[], + reasoning=False, + ) + + response = service.chat(request) + + # Should have 2 operations: add_sketch and extrude + assert len(response.operations_executed) == 2 + assert all(op.status == "ok" for op in response.operations_executed) + assert response.operations_executed[0].tool == "add_sketch" + assert response.operations_executed[1].tool == "extrude" + + # Verify the dimensions - sketch should be 30x30 and extrude depth 30 + sketch_op = response.operations_executed[0] + extrude_op = response.operations_executed[1] + assert extrude_op.arguments["depth"] == 30.0 + + # Sketch entities should form a 30x30 square + entities = sketch_op.arguments["entities"] + line_1 = entities["l1"] + assert line_1["end"][0] == 30.0 # length is 30 + + +def test_chat_api_can_generate_cube_code() -> None: + client = TestClient(app) + payload = { + "message": "Generate a 30x30x30 cube", + "tree_state": _seed_tree().model_dump(), + "conversation_history": [], + "reasoning": False, + "generate_code": True, + } + + response = client.post("/chat", json=payload) + assert response.status_code == 200 + + body = response.json() + assert body["operations_executed"] == [] + assert '.box(30.0, 30.0, 30.0, name="Cube")' in body["generated_code"] + assert "from opencad import Part" in body["generated_code"] From b523a4857bbd31c582d94d014155a06a1d37d8ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:36:10 +0000 Subject: [PATCH 3/3] Improve test coverage for cube sketch verification Co-authored-by: deanhu0822 <86739329+deanhu0822@users.noreply.github.com> --- opencad_agent/tests/test_agent.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/opencad_agent/tests/test_agent.py b/opencad_agent/tests/test_agent.py index 2aa6419..5f3fd81 100644 --- a/opencad_agent/tests/test_agent.py +++ b/opencad_agent/tests/test_agent.py @@ -337,10 +337,16 @@ def test_planner_execute_handles_cube_request() -> None: extrude_op = response.operations_executed[1] assert extrude_op.arguments["depth"] == 30.0 - # Sketch entities should form a 30x30 square + # Verify sketch forms a complete 30x30 square entities = sketch_op.arguments["entities"] - line_1 = entities["l1"] - assert line_1["end"][0] == 30.0 # length is 30 + assert entities["l1"]["start"] == (0.0, 0.0) + assert entities["l1"]["end"] == (30.0, 0.0) + assert entities["l2"]["start"] == (30.0, 0.0) + assert entities["l2"]["end"] == (30.0, 30.0) + assert entities["l3"]["start"] == (30.0, 30.0) + assert entities["l3"]["end"] == (0.0, 30.0) + assert entities["l4"]["start"] == (0.0, 30.0) + assert entities["l4"]["end"] == (0.0, 0.0) def test_chat_api_can_generate_cube_code() -> None: