Spaces:
Sleeping
Sleeping
Commit ·
eb89dca
1
Parent(s): 8f354fe
feat: add DesignPlan model, compute_score(), phase/plan fields to DesignState
Browse files- agents/design_state.py +79 -0
- 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"
|