CallMeDaniel Claude Opus 4.6 (1M context) commited on
Commit
eb89dca
·
1 Parent(s): 8f354fe

feat: add DesignPlan model, compute_score(), phase/plan fields to DesignState

Browse files
Files changed (2) hide show
  1. agents/design_state.py +79 -0
  2. tests/test_design_state.py +104 -1
agents/design_state.py CHANGED
@@ -37,6 +37,59 @@ def _get_dim_contexts() -> dict[str, str]:
37
  }
38
 
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  class DesignState(BaseModel):
41
  """Structured state tracking design decisions across chat turns."""
42
  part_name: str = ""
@@ -47,6 +100,8 @@ class DesignState(BaseModel):
47
  constraints: list[str] = Field(default_factory=list)
48
  decisions: list[str] = Field(default_factory=list)
49
  axis_recommendation: str = ""
 
 
50
 
51
  def render(self) -> str:
52
  """Render non-empty fields as a concise spec block for LLM context."""
@@ -188,6 +243,30 @@ class DesignState(BaseModel):
188
  return state
189
 
190
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  def extract_decisions(
192
  agent_responses: list[dict],
193
  current_state: DesignState,
 
37
  }
38
 
39
 
40
+ class DesignPlan(BaseModel):
41
+ """Structured plan presented to user for review/editing before generation."""
42
+ part_name: str = ""
43
+ description: str = ""
44
+ material: str = ""
45
+ dimensions: dict[str, float] = Field(default_factory=dict)
46
+ features: list[str] = Field(default_factory=list)
47
+ constraints: list[str] = Field(default_factory=list)
48
+ axis_recommendation: str = ""
49
+ machining_notes: list[str] = Field(default_factory=list)
50
+ confidence_score: float = 0.0
51
+
52
+ @classmethod
53
+ def from_state(cls, state: "DesignState", confidence_score: float) -> "DesignPlan":
54
+ """Create a plan snapshot from the current design state."""
55
+ return cls(
56
+ part_name=state.part_name,
57
+ description=state.description,
58
+ material=state.material,
59
+ dimensions=dict(state.dimensions),
60
+ features=list(state.features),
61
+ constraints=list(state.constraints),
62
+ axis_recommendation=state.axis_recommendation,
63
+ machining_notes=[],
64
+ confidence_score=confidence_score,
65
+ )
66
+
67
+ def render_approved(self) -> str:
68
+ """Render as an approved plan context block for LLM agents."""
69
+ lines = ["## APPROVED DESIGN PLAN (user-confirmed)"]
70
+ if self.part_name:
71
+ lines.append(f"Part: {self.part_name}")
72
+ if self.description:
73
+ lines.append(f"Description: {self.description}")
74
+ if self.material:
75
+ lines.append(f"Material: {self.material}")
76
+ if self.dimensions:
77
+ dims = ", ".join(f"{k}={v}mm" for k, v in self.dimensions.items())
78
+ lines.append(f"Dimensions: {dims}")
79
+ if self.features:
80
+ lines.append(f"Features: {'; '.join(self.features)}")
81
+ if self.constraints:
82
+ lines.append(f"Constraints: {'; '.join(self.constraints)}")
83
+ if self.axis_recommendation:
84
+ lines.append(f"Axis: {self.axis_recommendation}")
85
+ if self.machining_notes:
86
+ lines.append(f"Machining Notes: {'; '.join(self.machining_notes)}")
87
+ lines.append("")
88
+ lines.append("This plan has been reviewed and approved by the user.")
89
+ lines.append("Generate the model according to these specifications.")
90
+ return "\n".join(lines)
91
+
92
+
93
  class DesignState(BaseModel):
94
  """Structured state tracking design decisions across chat turns."""
95
  part_name: str = ""
 
100
  constraints: list[str] = Field(default_factory=list)
101
  decisions: list[str] = Field(default_factory=list)
102
  axis_recommendation: str = ""
103
+ phase: str = "exploring"
104
+ plan: DesignPlan | None = None
105
 
106
  def render(self) -> str:
107
  """Render non-empty fields as a concise spec block for LLM context."""
 
243
  return state
244
 
245
 
246
+ def compute_score(state: DesignState) -> float:
247
+ """Compute completeness score for a design state using configured weights and caps."""
248
+ cfg = settings.planning
249
+ score = 0.0
250
+ if state.material:
251
+ score += cfg.weights.get("material", 3)
252
+ if state.part_name:
253
+ score += cfg.weights.get("part_name", 1)
254
+ if state.description:
255
+ score += cfg.weights.get("description", 1)
256
+ if state.axis_recommendation:
257
+ score += cfg.weights.get("axis_recommendation", 2)
258
+ dim_cap = cfg.caps.get("dimension", 4)
259
+ dim_count = min(len(state.dimensions), dim_cap)
260
+ score += dim_count * cfg.weights.get("dimension", 1)
261
+ feat_cap = cfg.caps.get("feature", 4)
262
+ feat_count = min(len(state.features), feat_cap)
263
+ score += feat_count * cfg.weights.get("feature", 1)
264
+ const_cap = cfg.caps.get("constraint", 2)
265
+ const_count = min(len(state.constraints), const_cap)
266
+ score += const_count * cfg.weights.get("constraint", 1)
267
+ return score
268
+
269
+
270
  def extract_decisions(
271
  agent_responses: list[dict],
272
  current_state: DesignState,
tests/test_design_state.py CHANGED
@@ -1,6 +1,6 @@
1
  """Tests for agents/design_state.py — state tracking and decision extraction."""
2
 
3
- from agents.design_state import DesignState, extract_decisions
4
 
5
 
6
  class TestDesignState:
@@ -88,3 +88,106 @@ class TestExtractDecisions:
88
  updated = extract_decisions(responses, existing)
89
  m6_count = sum(1 for f in updated.features if "M6" in f)
90
  assert m6_count == 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """Tests for agents/design_state.py — state tracking and decision extraction."""
2
 
3
+ from agents.design_state import DesignPlan, DesignState, compute_score, extract_decisions
4
 
5
 
6
  class TestDesignState:
 
88
  updated = extract_decisions(responses, existing)
89
  m6_count = sum(1 for f in updated.features if "M6" in f)
90
  assert m6_count == 1
91
+
92
+
93
+ class TestDesignPlan:
94
+ def test_create_from_state(self):
95
+ state = DesignState(
96
+ part_name="bracket",
97
+ description="mounting bracket",
98
+ material="aluminum 6061",
99
+ dimensions={"width": 60.0, "height": 40.0, "depth": 20.0},
100
+ features=["4x M6 holes"],
101
+ constraints=["min wall 3mm"],
102
+ axis_recommendation="3-axis",
103
+ decisions=["use aluminum"],
104
+ )
105
+ plan = DesignPlan.from_state(state, confidence_score=9.0)
106
+ assert plan.part_name == "bracket"
107
+ assert plan.material == "aluminum 6061"
108
+ assert plan.dimensions == {"width": 60.0, "height": 40.0, "depth": 20.0}
109
+ assert plan.confidence_score == 9.0
110
+ assert plan.machining_notes == []
111
+
112
+ def test_plan_render(self):
113
+ plan = DesignPlan(
114
+ part_name="bracket",
115
+ description="test",
116
+ material="aluminum 6061",
117
+ dimensions={"width": 60.0},
118
+ features=["4x M6 holes"],
119
+ constraints=[],
120
+ axis_recommendation="3-axis",
121
+ machining_notes=["No undercuts"],
122
+ confidence_score=9.0,
123
+ )
124
+ rendered = plan.render_approved()
125
+ assert "APPROVED DESIGN PLAN" in rendered
126
+ assert "aluminum 6061" in rendered
127
+ assert "No undercuts" in rendered
128
+
129
+
130
+ class TestComputeScore:
131
+ def test_empty_state_scores_zero(self):
132
+ assert compute_score(DesignState()) == 0.0
133
+
134
+ def test_material_scores_3(self):
135
+ state = DesignState(material="aluminum")
136
+ assert compute_score(state) == 3.0
137
+
138
+ def test_full_state_above_threshold(self):
139
+ state = DesignState(
140
+ part_name="bracket",
141
+ description="test bracket",
142
+ material="aluminum 6061",
143
+ dimensions={"width": 60.0, "height": 40.0, "depth": 20.0},
144
+ features=["4x M6 holes"],
145
+ constraints=["min wall 3mm"],
146
+ axis_recommendation="3-axis",
147
+ )
148
+ score = compute_score(state)
149
+ assert score >= 8.0
150
+
151
+ def test_dimension_cap_at_4(self):
152
+ state = DesignState(dimensions={
153
+ "width": 60, "height": 40, "depth": 20,
154
+ "length": 100, "diameter": 10, "radius": 5,
155
+ })
156
+ score = compute_score(state)
157
+ assert score == 4.0
158
+
159
+ def test_feature_cap_at_4(self):
160
+ state = DesignState(features=["a", "b", "c", "d", "e", "f"])
161
+ score = compute_score(state)
162
+ assert score == 4.0
163
+
164
+ def test_constraint_cap_at_2(self):
165
+ state = DesignState(constraints=["a", "b", "c", "d"])
166
+ score = compute_score(state)
167
+ assert score == 2.0
168
+
169
+
170
+ class TestDesignStatePhase:
171
+ def test_default_phase_exploring(self):
172
+ state = DesignState()
173
+ assert state.phase == "exploring"
174
+ assert state.plan is None
175
+
176
+ def test_phase_serialization(self):
177
+ plan = DesignPlan(
178
+ part_name="b", description="", material="steel",
179
+ dimensions={}, features=[], constraints=[],
180
+ axis_recommendation="", machining_notes=[],
181
+ confidence_score=5.0,
182
+ )
183
+ state = DesignState(phase="planning", plan=plan)
184
+ d = state.model_dump()
185
+ assert d["phase"] == "planning"
186
+ assert d["plan"]["material"] == "steel"
187
+
188
+ def test_roundtrip_from_dict(self):
189
+ state = DesignState(phase="approved", material="brass")
190
+ d = state.model_dump()
191
+ restored = DesignState(**d)
192
+ assert restored.phase == "approved"
193
+ assert restored.material == "brass"