CallMeDaniel Claude Opus 4.6 (1M context) commited on
Commit
cdccbe3
·
1 Parent(s): 1923201

refactor: add ToolConfig pydantic model, replace tool_config dicts

Browse files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files changed (3) hide show
  1. agents/tools.py +2 -2
  2. core/cam.py +26 -18
  3. tests/test_cam.py +48 -12
agents/tools.py CHANGED
@@ -97,11 +97,11 @@ class GenerateGcodeTool(BaseTool):
97
  args_schema: Type[BaseModel] = GenerateGcodeInput
98
 
99
  def _run(self, operations: list[str], tool_diameter: float = 6.0, post_processor: str = "grbl") -> str:
100
- from core.cam import generate_gcode
101
  shape = get_last_shape()
102
  if shape is None:
103
  return json.dumps({"success": False, "error": "No shape available. Run Execute CadQuery Code first."})
104
- tool_config = {"diameter": tool_diameter, "h_feed": 800, "v_feed": 200, "speed": 18000}
105
  result = generate_gcode(
106
  shape=shape, operations=operations,
107
  tool_config=tool_config, post_processor=post_processor,
 
97
  args_schema: Type[BaseModel] = GenerateGcodeInput
98
 
99
  def _run(self, operations: list[str], tool_diameter: float = 6.0, post_processor: str = "grbl") -> str:
100
+ from core.cam import generate_gcode, ToolConfig
101
  shape = get_last_shape()
102
  if shape is None:
103
  return json.dumps({"success": False, "error": "No shape available. Run Execute CadQuery Code first."})
104
+ tool_config = ToolConfig(diameter=tool_diameter, h_feed=800, v_feed=200, speed=18000)
105
  result = generate_gcode(
106
  shape=shape, operations=operations,
107
  tool_config=tool_config, post_processor=post_processor,
core/cam.py CHANGED
@@ -9,12 +9,20 @@ from __future__ import annotations
9
  from pydantic import BaseModel, Field
10
 
11
 
 
 
 
 
 
 
 
 
12
  class CAMResult(BaseModel):
13
  """Result of G-code generation from a CadQuery shape."""
14
  success: bool
15
  gcode: str | None = None
16
  operations: list[str] = Field(default_factory=list)
17
- tool_config: dict = Field(default_factory=dict)
18
  post_processor: str = "grbl"
19
  error: str | None = None
20
 
@@ -29,25 +37,25 @@ class CAMPlan(BaseModel):
29
  tool_speed: float = Field(default=18000, description="Spindle speed RPM")
30
  post_processor: str = Field(default="grbl", description="G-code format")
31
 
32
- def to_tool_config(self) -> dict:
33
- """Convert to the dict format expected by generate_gcode()."""
34
- return {
35
- "diameter": self.tool_diameter,
36
- "h_feed": self.tool_h_feed,
37
- "v_feed": self.tool_v_feed,
38
- "speed": self.tool_speed,
39
- }
40
 
41
 
42
  from config.settings import settings
43
 
44
 
45
- def _get_default_tool_config() -> dict:
46
  """Load default roughing tool config from config.yaml cam section."""
47
  roughing = settings.cam.tools.get("roughing")
48
  if roughing:
49
- return roughing.model_dump()
50
- return {"diameter": 6, "h_feed": 800, "v_feed": 200, "speed": 18000}
51
 
52
 
53
  def _get_default_post_processor() -> str:
@@ -63,7 +71,7 @@ def _get_stock_offset() -> float:
63
  def generate_gcode(
64
  shape,
65
  operations: list[str],
66
- tool_config: dict | None = None,
67
  post_processor: str | None = None,
68
  stock_offset_mm: float | None = None,
69
  ) -> CAMResult:
@@ -73,7 +81,7 @@ def generate_gcode(
73
  shape: A cq.Workplane object from the executor.
74
  operations: List of operation names: "adaptive", "pocket", "profile",
75
  "face", "drill", "surface", "waterline".
76
- tool_config: Dict with diameter, h_feed, v_feed, speed. Uses config defaults if None.
77
  post_processor: Post-processor name (e.g. "grbl", "linuxcnc"). Uses config default if None.
78
  stock_offset_mm: Stock offset from bounding box in mm. Uses config default if None.
79
 
@@ -105,10 +113,10 @@ def generate_gcode(
105
 
106
  try:
107
  tool = Endmill(
108
- diameter=tool_config.get("diameter", 6),
109
- h_feed=tool_config.get("h_feed", 800),
110
- v_feed=tool_config.get("v_feed", 200),
111
- speed=tool_config.get("speed", 18000),
112
  )
113
 
114
  so = stock_offset_mm
 
9
  from pydantic import BaseModel, Field
10
 
11
 
12
+ class ToolConfig(BaseModel):
13
+ """CNC tool configuration for G-code generation."""
14
+ diameter: float = 6.0
15
+ h_feed: float = 800
16
+ v_feed: float = 200
17
+ speed: float = 18000
18
+
19
+
20
  class CAMResult(BaseModel):
21
  """Result of G-code generation from a CadQuery shape."""
22
  success: bool
23
  gcode: str | None = None
24
  operations: list[str] = Field(default_factory=list)
25
+ tool_config: ToolConfig = Field(default_factory=ToolConfig)
26
  post_processor: str = "grbl"
27
  error: str | None = None
28
 
 
37
  tool_speed: float = Field(default=18000, description="Spindle speed RPM")
38
  post_processor: str = Field(default="grbl", description="G-code format")
39
 
40
+ def to_tool_config(self) -> ToolConfig:
41
+ """Convert to ToolConfig for generate_gcode()."""
42
+ return ToolConfig(
43
+ diameter=self.tool_diameter,
44
+ h_feed=self.tool_h_feed,
45
+ v_feed=self.tool_v_feed,
46
+ speed=self.tool_speed,
47
+ )
48
 
49
 
50
  from config.settings import settings
51
 
52
 
53
+ def _get_default_tool_config() -> ToolConfig:
54
  """Load default roughing tool config from config.yaml cam section."""
55
  roughing = settings.cam.tools.get("roughing")
56
  if roughing:
57
+ return ToolConfig(**roughing.model_dump())
58
+ return ToolConfig()
59
 
60
 
61
  def _get_default_post_processor() -> str:
 
71
  def generate_gcode(
72
  shape,
73
  operations: list[str],
74
+ tool_config: ToolConfig | None = None,
75
  post_processor: str | None = None,
76
  stock_offset_mm: float | None = None,
77
  ) -> CAMResult:
 
81
  shape: A cq.Workplane object from the executor.
82
  operations: List of operation names: "adaptive", "pocket", "profile",
83
  "face", "drill", "surface", "waterline".
84
+ tool_config: ToolConfig with diameter, h_feed, v_feed, speed. Uses config defaults if None.
85
  post_processor: Post-processor name (e.g. "grbl", "linuxcnc"). Uses config default if None.
86
  stock_offset_mm: Stock offset from bounding box in mm. Uses config default if None.
87
 
 
113
 
114
  try:
115
  tool = Endmill(
116
+ diameter=tool_config.diameter,
117
+ h_feed=tool_config.h_feed,
118
+ v_feed=tool_config.v_feed,
119
+ speed=tool_config.speed,
120
  )
121
 
122
  so = stock_offset_mm
tests/test_cam.py CHANGED
@@ -1,6 +1,27 @@
1
  """Tests for core/cam.py — CAM engine."""
2
 
3
- from core.cam import CAMResult
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
 
6
  class TestCAMResult:
@@ -9,7 +30,7 @@ class TestCAMResult:
9
  success=True,
10
  gcode="G21 G90\nG00 X0 Y0 Z10\nM30",
11
  operations=["pocket", "profile"],
12
- tool_config={"diameter": 6, "h_feed": 800},
13
  post_processor="grbl",
14
  )
15
  assert r.success is True
@@ -26,16 +47,20 @@ class TestCAMResult:
26
  def test_model_dump(self):
27
  r = CAMResult(
28
  success=True, gcode="G21 G90\nM30",
29
- operations=["pocket"], tool_config={"diameter": 6},
30
  )
31
  d = r.model_dump()
32
  assert d["success"] is True
33
  assert d["gcode"] == "G21 G90\nM30"
34
  assert d["operations"] == ["pocket"]
35
- assert d["tool_config"] == {"diameter": 6}
36
  assert d["post_processor"] == "grbl"
37
  assert d["error"] is None
38
 
 
 
 
 
39
 
40
  from unittest.mock import patch, MagicMock
41
  from core.cam import generate_gcode
@@ -48,7 +73,7 @@ class TestGenerateGcode:
48
  result = generate_gcode(
49
  shape=mock_shape,
50
  operations=["pocket"],
51
- tool_config={"diameter": 6, "h_feed": 800, "v_feed": 200, "speed": 18000},
52
  post_processor="grbl",
53
  )
54
  assert result.success is False
@@ -71,13 +96,25 @@ class TestGenerateGcode:
71
  tool_config=None,
72
  )
73
  assert result.tool_config is not None
74
- assert "diameter" in result.tool_config
 
75
 
76
  def test_uses_default_post_processor(self):
77
  mock_shape = MagicMock()
78
  result = generate_gcode(shape=mock_shape, operations=["pocket"])
79
  assert result.post_processor == "grbl"
80
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
  from core.cam import CAMPlan
83
 
@@ -107,9 +144,8 @@ class TestCAMPlan:
107
  def test_to_tool_config(self):
108
  plan = CAMPlan(operations=["pocket"])
109
  config = plan.to_tool_config()
110
- assert config == {
111
- "diameter": 6.0,
112
- "h_feed": 800,
113
- "v_feed": 200,
114
- "speed": 18000,
115
- }
 
1
  """Tests for core/cam.py — CAM engine."""
2
 
3
+ from core.cam import CAMResult, ToolConfig
4
+
5
+
6
+ class TestToolConfig:
7
+ def test_defaults(self):
8
+ tc = ToolConfig()
9
+ assert tc.diameter == 6.0
10
+ assert tc.h_feed == 800
11
+ assert tc.v_feed == 200
12
+ assert tc.speed == 18000
13
+
14
+ def test_custom_values(self):
15
+ tc = ToolConfig(diameter=3.0, h_feed=400, v_feed=100, speed=24000)
16
+ assert tc.diameter == 3.0
17
+ assert tc.h_feed == 400
18
+ assert tc.v_feed == 100
19
+ assert tc.speed == 24000
20
+
21
+ def test_model_dump(self):
22
+ tc = ToolConfig(diameter=10.0, h_feed=1000, v_feed=300, speed=12000)
23
+ d = tc.model_dump()
24
+ assert d == {"diameter": 10.0, "h_feed": 1000, "v_feed": 300, "speed": 12000}
25
 
26
 
27
  class TestCAMResult:
 
30
  success=True,
31
  gcode="G21 G90\nG00 X0 Y0 Z10\nM30",
32
  operations=["pocket", "profile"],
33
+ tool_config=ToolConfig(diameter=6, h_feed=800),
34
  post_processor="grbl",
35
  )
36
  assert r.success is True
 
47
  def test_model_dump(self):
48
  r = CAMResult(
49
  success=True, gcode="G21 G90\nM30",
50
+ operations=["pocket"], tool_config=ToolConfig(diameter=6),
51
  )
52
  d = r.model_dump()
53
  assert d["success"] is True
54
  assert d["gcode"] == "G21 G90\nM30"
55
  assert d["operations"] == ["pocket"]
56
+ assert d["tool_config"] == {"diameter": 6.0, "h_feed": 800, "v_feed": 200, "speed": 18000}
57
  assert d["post_processor"] == "grbl"
58
  assert d["error"] is None
59
 
60
+ def test_default_tool_config_is_toolconfig(self):
61
+ r = CAMResult(success=True)
62
+ assert isinstance(r.tool_config, ToolConfig)
63
+
64
 
65
  from unittest.mock import patch, MagicMock
66
  from core.cam import generate_gcode
 
73
  result = generate_gcode(
74
  shape=mock_shape,
75
  operations=["pocket"],
76
+ tool_config=ToolConfig(diameter=6, h_feed=800, v_feed=200, speed=18000),
77
  post_processor="grbl",
78
  )
79
  assert result.success is False
 
96
  tool_config=None,
97
  )
98
  assert result.tool_config is not None
99
+ assert isinstance(result.tool_config, ToolConfig)
100
+ assert result.tool_config.diameter == 6.0
101
 
102
  def test_uses_default_post_processor(self):
103
  mock_shape = MagicMock()
104
  result = generate_gcode(shape=mock_shape, operations=["pocket"])
105
  assert result.post_processor == "grbl"
106
 
107
+ def test_tool_config_attribute_access(self):
108
+ mock_shape = MagicMock()
109
+ tc = ToolConfig(diameter=3.0, h_feed=400, v_feed=100, speed=24000)
110
+ result = generate_gcode(
111
+ shape=mock_shape,
112
+ operations=["pocket"],
113
+ tool_config=tc,
114
+ )
115
+ assert result.tool_config.diameter == 3.0
116
+ assert result.tool_config.h_feed == 400
117
+
118
 
119
  from core.cam import CAMPlan
120
 
 
144
  def test_to_tool_config(self):
145
  plan = CAMPlan(operations=["pocket"])
146
  config = plan.to_tool_config()
147
+ assert isinstance(config, ToolConfig)
148
+ assert config.diameter == 6.0
149
+ assert config.h_feed == 800
150
+ assert config.v_feed == 200
151
+ assert config.speed == 18000