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

refactor: type analyze_gaps and extract_decisions with AgentResponse

Browse files

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

agents/crew_orchestrator.py CHANGED
@@ -267,11 +267,10 @@ class CrewOrchestrator(BaseOrchestrator):
267
  preview["gcode_url"] = f"/api/models/{part_name}.gcode"
268
 
269
  # Update design state
270
- agent_msgs = [{"message": r.get("message", "")} for r in responses]
271
- updated_state = extract_decisions(agent_msgs, state, message)
272
 
273
  # Gap analysis — detect missing info and generate question cards
274
- gap_result = analyze_gaps(responses)
275
  question_cards = []
276
  if gap_result.has_gaps:
277
  cards = generate_question_cards(gap_result, updated_state, user_message=message)
@@ -287,8 +286,8 @@ class CrewOrchestrator(BaseOrchestrator):
287
 
288
  # If approved and CAD said NOT READY, reset
289
  if state.phase == "approved":
290
- for r in responses:
291
- if r.get("agent_id") == "cad" and r.get("message", "").upper().startswith("NOT READY:"):
292
  updated_state.phase = "exploring"
293
  updated_state.plan = None
294
  break
@@ -322,7 +321,7 @@ class CrewOrchestrator(BaseOrchestrator):
322
  mock = MockChatBackend(output_dir=self.output_dir)
323
  result = mock.chat_turn(message, history, mentions, design_state=state.model_dump(), plan_context=plan_context)
324
  if "question_cards" not in result:
325
- result_responses = result.get("responses", [])
326
  gap_result = analyze_gaps(result_responses)
327
  if gap_result.has_gaps:
328
  cards = generate_question_cards(gap_result, state, user_message=message)
 
267
  preview["gcode_url"] = f"/api/models/{part_name}.gcode"
268
 
269
  # Update design state
270
+ updated_state = extract_decisions(agent_responses, state, message)
 
271
 
272
  # Gap analysis — detect missing info and generate question cards
273
+ gap_result = analyze_gaps(agent_responses)
274
  question_cards = []
275
  if gap_result.has_gaps:
276
  cards = generate_question_cards(gap_result, updated_state, user_message=message)
 
286
 
287
  # If approved and CAD said NOT READY, reset
288
  if state.phase == "approved":
289
+ for r in agent_responses:
290
+ if r.agent_id == "cad" and r.message.upper().startswith("NOT READY:"):
291
  updated_state.phase = "exploring"
292
  updated_state.plan = None
293
  break
 
321
  mock = MockChatBackend(output_dir=self.output_dir)
322
  result = mock.chat_turn(message, history, mentions, design_state=state.model_dump(), plan_context=plan_context)
323
  if "question_cards" not in result:
324
+ result_responses = [AgentResponse(**r) for r in result.get("responses", [])]
325
  gap_result = analyze_gaps(result_responses)
326
  if gap_result.has_gaps:
327
  cards = generate_question_cards(gap_result, state, user_message=message)
agents/design_state.py CHANGED
@@ -3,12 +3,15 @@
3
  from __future__ import annotations
4
 
5
  import re
6
- from typing import Literal
7
 
8
  from pydantic import BaseModel, Field
9
 
10
  from config.settings import settings
11
 
 
 
 
12
 
13
  # ── Config-driven domain data helpers ─────────────────────────────────────────
14
 
@@ -135,7 +138,7 @@ class DesignState(BaseModel):
135
 
136
  def update_from_messages(
137
  self,
138
- agent_responses: list[dict],
139
  user_message: str = "",
140
  ) -> "DesignState":
141
  """Extract design decisions from messages and return updated state."""
@@ -143,7 +146,7 @@ class DesignState(BaseModel):
143
  max_decisions = settings.orchestration.max_decisions
144
 
145
  # Combine all text for scanning
146
- all_text = user_message + " " + " ".join(r.get("message", "") for r in agent_responses)
147
  lower = all_text.lower()
148
 
149
  materials = _get_materials()
@@ -217,7 +220,7 @@ class DesignState(BaseModel):
217
 
218
  # Extract decisions: sentences with agreement language from agent messages only
219
  for resp in agent_responses:
220
- msg = resp.get("message", "")
221
  sentences = re.split(r'[.!?]+', msg)
222
  for sentence in sentences:
223
  s = sentence.strip()
@@ -273,7 +276,7 @@ def compute_score(state: DesignState) -> float:
273
 
274
 
275
  def extract_decisions(
276
- agent_responses: list[dict],
277
  current_state: DesignState,
278
  user_message: str = "",
279
  ) -> DesignState:
 
3
  from __future__ import annotations
4
 
5
  import re
6
+ from typing import TYPE_CHECKING, Literal
7
 
8
  from pydantic import BaseModel, Field
9
 
10
  from config.settings import settings
11
 
12
+ if TYPE_CHECKING:
13
+ from agents.agent_flow import AgentResponse
14
+
15
 
16
  # ── Config-driven domain data helpers ─────────────────────────────────────────
17
 
 
138
 
139
  def update_from_messages(
140
  self,
141
+ agent_responses: list[AgentResponse],
142
  user_message: str = "",
143
  ) -> "DesignState":
144
  """Extract design decisions from messages and return updated state."""
 
146
  max_decisions = settings.orchestration.max_decisions
147
 
148
  # Combine all text for scanning
149
+ all_text = user_message + " " + " ".join(r.message for r in agent_responses)
150
  lower = all_text.lower()
151
 
152
  materials = _get_materials()
 
220
 
221
  # Extract decisions: sentences with agreement language from agent messages only
222
  for resp in agent_responses:
223
+ msg = resp.message
224
  sentences = re.split(r'[.!?]+', msg)
225
  for sentence in sentences:
226
  s = sentence.strip()
 
276
 
277
 
278
  def extract_decisions(
279
+ agent_responses: list[AgentResponse],
280
  current_state: DesignState,
281
  user_message: str = "",
282
  ) -> DesignState:
agents/gap_analyzer.py CHANGED
@@ -10,6 +10,7 @@ from pydantic import BaseModel, Field
10
 
11
  from config.settings import settings
12
  from agents.definitions import AGENTS
 
13
 
14
 
15
  # ── Models ────────────────────────────────────────────────────────────────────
@@ -85,7 +86,7 @@ QUESTION_TEMPLATES: dict[str, dict] = {
85
  # ── Core functions ────────────────────────────────────────────────────────────
86
 
87
 
88
- def analyze_gaps(responses: list[dict]) -> GapAnalysis:
89
  """Scan agent responses for NOT READY: prefixes and classify missing items.
90
 
91
  Deduplicates by category — first agent to flag a category wins.
@@ -97,8 +98,8 @@ def analyze_gaps(responses: list[dict]) -> GapAnalysis:
97
  missing_items: list[MissingItem] = []
98
 
99
  for response in responses:
100
- message: str = response.get("message", "")
101
- agent_id: str = response.get("agent_id", "")
102
 
103
  # Case-insensitive check for NOT READY: prefix
104
  stripped = message.strip()
 
10
 
11
  from config.settings import settings
12
  from agents.definitions import AGENTS
13
+ from agents.agent_flow import AgentResponse
14
 
15
 
16
  # ── Models ────────────────────────────────────────────────────────────────────
 
86
  # ── Core functions ────────────────────────────────────────────────────────────
87
 
88
 
89
+ def analyze_gaps(responses: list[AgentResponse]) -> GapAnalysis:
90
  """Scan agent responses for NOT READY: prefixes and classify missing items.
91
 
92
  Deduplicates by category — first agent to flag a category wins.
 
98
  missing_items: list[MissingItem] = []
99
 
100
  for response in responses:
101
+ message: str = response.message
102
+ agent_id: str = response.agent_id
103
 
104
  # Case-insensitive check for NOT READY: prefix
105
  stripped = message.strip()
agents/orchestrator.py CHANGED
@@ -18,7 +18,7 @@ from pathlib import Path
18
 
19
  from agents.definitions import AGENT_COLORS, AGENT_NAMES, AGENT_AVATARS
20
  from core.utils import derive_part_name
21
- from agents.agent_flow import route_agents
22
  from agents.base import BaseOrchestrator
23
  from agents.design_state import DesignState, extract_decisions
24
  from core.backends import MockBackend
@@ -176,22 +176,23 @@ class MockChatBackend(BaseOrchestrator):
176
  else:
177
  active = route_agents(message, mentions=[], is_approved_phase=False)
178
 
 
179
  responses: list[dict] = []
180
  preview = None
181
 
182
  if "design" in active:
183
- responses.append(
184
- _format_response("design", self._design_response(lower))
185
  )
186
 
187
  if "engineering" in active:
188
- responses.append(
189
- _format_response("engineering", self._engineering_response(lower))
190
  )
191
 
192
  if "cnc" in active:
193
- responses.append(
194
- _format_response("cnc", self._cnc_response(lower))
195
  )
196
 
197
  if "cad" in active:
@@ -200,8 +201,8 @@ class MockChatBackend(BaseOrchestrator):
200
 
201
  mock = MockBackend()
202
  code = mock.generate(build_messages(message))
203
- responses.append(
204
- _format_response(
205
  "cad",
206
  "Model generated. Click the 3D viewer to inspect it.",
207
  code=code,
@@ -209,8 +210,10 @@ class MockChatBackend(BaseOrchestrator):
209
  )
210
  preview = _execute_cad_code(code, message, self.output_dir)
211
 
 
 
212
  # Update design state from responses
213
- updated_state = extract_decisions(responses, state, message)
214
 
215
  return {"responses": responses, "preview": preview, "design_state": updated_state.model_dump()}
216
 
 
18
 
19
  from agents.definitions import AGENT_COLORS, AGENT_NAMES, AGENT_AVATARS
20
  from core.utils import derive_part_name
21
+ from agents.agent_flow import AgentResponse, route_agents
22
  from agents.base import BaseOrchestrator
23
  from agents.design_state import DesignState, extract_decisions
24
  from core.backends import MockBackend
 
176
  else:
177
  active = route_agents(message, mentions=[], is_approved_phase=False)
178
 
179
+ agent_responses: list[AgentResponse] = []
180
  responses: list[dict] = []
181
  preview = None
182
 
183
  if "design" in active:
184
+ agent_responses.append(
185
+ AgentResponse.from_agent("design", self._design_response(lower))
186
  )
187
 
188
  if "engineering" in active:
189
+ agent_responses.append(
190
+ AgentResponse.from_agent("engineering", self._engineering_response(lower))
191
  )
192
 
193
  if "cnc" in active:
194
+ agent_responses.append(
195
+ AgentResponse.from_agent("cnc", self._cnc_response(lower))
196
  )
197
 
198
  if "cad" in active:
 
201
 
202
  mock = MockBackend()
203
  code = mock.generate(build_messages(message))
204
+ agent_responses.append(
205
+ AgentResponse.from_agent(
206
  "cad",
207
  "Model generated. Click the 3D viewer to inspect it.",
208
  code=code,
 
210
  )
211
  preview = _execute_cad_code(code, message, self.output_dir)
212
 
213
+ responses = [r.model_dump() for r in agent_responses]
214
+
215
  # Update design state from responses
216
+ updated_state = extract_decisions(agent_responses, state, message)
217
 
218
  return {"responses": responses, "preview": preview, "design_state": updated_state.model_dump()}
219
 
tests/test_design_state.py CHANGED
@@ -1,5 +1,6 @@
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
 
@@ -34,7 +35,7 @@ class TestDesignState:
34
  class TestExtractDecisions:
35
  def test_extracts_material(self):
36
  responses = [
37
- {"agent_id": "engineering", "message": "I recommend aluminum 6061 for this application."}
38
  ]
39
  state = extract_decisions(responses, DesignState())
40
  assert "aluminum" in state.material.lower()
@@ -47,14 +48,14 @@ class TestExtractDecisions:
47
 
48
  def test_extracts_fastener_features(self):
49
  responses = [
50
- {"agent_id": "engineering", "message": "I'll add 4x M6 clearance holes for mounting."}
51
  ]
52
  state = extract_decisions(responses, DesignState())
53
  assert any("M6" in f for f in state.features)
54
 
55
  def test_extracts_axis_recommendation(self):
56
  responses = [
57
- {"agent_id": "cnc", "message": "This part needs 5-axis machining due to the undercut."}
58
  ]
59
  state = extract_decisions(responses, DesignState())
60
  assert "5-axis" in state.axis_recommendation
@@ -67,7 +68,7 @@ class TestExtractDecisions:
67
  def test_preserves_existing_state(self):
68
  existing = DesignState(material="steel", dimensions={"width": 50.0})
69
  responses = [
70
- {"agent_id": "engineering", "message": "Height should be 30mm."}
71
  ]
72
  updated = extract_decisions(responses, existing, user_message="add height")
73
  assert updated.material == "steel"
@@ -75,7 +76,7 @@ class TestExtractDecisions:
75
 
76
  def test_extracts_decisions_from_agreement(self):
77
  responses = [
78
- {"agent_id": "design", "message": "I'd recommend an L-bracket form factor for this."}
79
  ]
80
  state = extract_decisions(responses, DesignState())
81
  assert len(state.decisions) > 0
@@ -83,7 +84,7 @@ class TestExtractDecisions:
83
  def test_no_duplicate_features(self):
84
  existing = DesignState(features=["4x M6 holes"])
85
  responses = [
86
- {"agent_id": "engineering", "message": "The 4x M6 holes are properly specified."}
87
  ]
88
  updated = extract_decisions(responses, existing)
89
  m6_count = sum(1 for f in updated.features if "M6" in f)
 
1
  """Tests for agents/design_state.py — state tracking and decision extraction."""
2
 
3
+ from agents.agent_flow import AgentResponse
4
  from agents.design_state import DesignPlan, DesignState, compute_score, extract_decisions
5
 
6
 
 
35
  class TestExtractDecisions:
36
  def test_extracts_material(self):
37
  responses = [
38
+ AgentResponse.from_agent("engineering", "I recommend aluminum 6061 for this application.")
39
  ]
40
  state = extract_decisions(responses, DesignState())
41
  assert "aluminum" in state.material.lower()
 
48
 
49
  def test_extracts_fastener_features(self):
50
  responses = [
51
+ AgentResponse.from_agent("engineering", "I'll add 4x M6 clearance holes for mounting.")
52
  ]
53
  state = extract_decisions(responses, DesignState())
54
  assert any("M6" in f for f in state.features)
55
 
56
  def test_extracts_axis_recommendation(self):
57
  responses = [
58
+ AgentResponse.from_agent("cnc", "This part needs 5-axis machining due to the undercut.")
59
  ]
60
  state = extract_decisions(responses, DesignState())
61
  assert "5-axis" in state.axis_recommendation
 
68
  def test_preserves_existing_state(self):
69
  existing = DesignState(material="steel", dimensions={"width": 50.0})
70
  responses = [
71
+ AgentResponse.from_agent("engineering", "Height should be 30mm.")
72
  ]
73
  updated = extract_decisions(responses, existing, user_message="add height")
74
  assert updated.material == "steel"
 
76
 
77
  def test_extracts_decisions_from_agreement(self):
78
  responses = [
79
+ AgentResponse.from_agent("design", "I'd recommend an L-bracket form factor for this.")
80
  ]
81
  state = extract_decisions(responses, DesignState())
82
  assert len(state.decisions) > 0
 
84
  def test_no_duplicate_features(self):
85
  existing = DesignState(features=["4x M6 holes"])
86
  responses = [
87
+ AgentResponse.from_agent("engineering", "The 4x M6 holes are properly specified.")
88
  ]
89
  updated = extract_decisions(responses, existing)
90
  m6_count = sum(1 for f in updated.features if "M6" in f)
tests/test_gap_analyzer.py CHANGED
@@ -1,5 +1,6 @@
1
  """Tests for agents/gap_analyzer.py — gap detection and question card generation."""
2
 
 
3
  from agents.gap_analyzer import (
4
  MissingItem, GapAnalysis, QuestionCard,
5
  analyze_gaps, generate_question_cards,
@@ -10,8 +11,8 @@ from agents.design_state import DesignState
10
  class TestAnalyzeGaps:
11
  def test_no_gaps_when_no_not_ready(self):
12
  responses = [
13
- {"agent_id": "design", "message": "I suggest an L-bracket design."},
14
- {"agent_id": "engineering", "message": "Aluminum 6061 would work well."},
15
  ]
16
  result = analyze_gaps(responses)
17
  assert not result.has_gaps
@@ -19,7 +20,7 @@ class TestAnalyzeGaps:
19
 
20
  def test_detects_not_ready_from_cad(self):
21
  responses = [
22
- {"agent_id": "cad", "message": "NOT READY: Need dimensions (width, height) and material selection."},
23
  ]
24
  result = analyze_gaps(responses)
25
  assert result.has_gaps
@@ -29,7 +30,7 @@ class TestAnalyzeGaps:
29
 
30
  def test_detects_not_ready_from_cnc(self):
31
  responses = [
32
- {"agent_id": "cnc", "message": "NOT READY: Need material and tolerance info for manufacturability check."},
33
  ]
34
  result = analyze_gaps(responses)
35
  assert result.has_gaps
@@ -39,7 +40,7 @@ class TestAnalyzeGaps:
39
 
40
  def test_detects_not_ready_from_cam(self):
41
  responses = [
42
- {"agent_id": "cam", "message": "NOT READY: No 3D model available. Need CAD generation first."},
43
  ]
44
  result = analyze_gaps(responses)
45
  assert result.has_gaps
@@ -48,8 +49,8 @@ class TestAnalyzeGaps:
48
 
49
  def test_deduplicates_across_agents(self):
50
  responses = [
51
- {"agent_id": "cad", "message": "NOT READY: Need material and dimensions."},
52
- {"agent_id": "cnc", "message": "NOT READY: Material is required for manufacturability."},
53
  ]
54
  result = analyze_gaps(responses)
55
  material_items = [i for i in result.missing_items if i.category == "material"]
@@ -58,7 +59,7 @@ class TestAnalyzeGaps:
58
 
59
  def test_case_insensitive_not_ready(self):
60
  responses = [
61
- {"agent_id": "cad", "message": "not ready: missing width and height dimensions."},
62
  ]
63
  result = analyze_gaps(responses)
64
  assert result.has_gaps
@@ -66,8 +67,8 @@ class TestAnalyzeGaps:
66
 
67
  def test_no_false_positive_on_regular_message(self):
68
  responses = [
69
- {"agent_id": "cad", "message": "Model generated successfully."},
70
- {"agent_id": "cnc", "message": "3-axis machining is ready for this part."},
71
  ]
72
  result = analyze_gaps(responses)
73
  assert not result.has_gaps
 
1
  """Tests for agents/gap_analyzer.py — gap detection and question card generation."""
2
 
3
+ from agents.agent_flow import AgentResponse
4
  from agents.gap_analyzer import (
5
  MissingItem, GapAnalysis, QuestionCard,
6
  analyze_gaps, generate_question_cards,
 
11
  class TestAnalyzeGaps:
12
  def test_no_gaps_when_no_not_ready(self):
13
  responses = [
14
+ AgentResponse.from_agent("design", "I suggest an L-bracket design."),
15
+ AgentResponse.from_agent("engineering", "Aluminum 6061 would work well."),
16
  ]
17
  result = analyze_gaps(responses)
18
  assert not result.has_gaps
 
20
 
21
  def test_detects_not_ready_from_cad(self):
22
  responses = [
23
+ AgentResponse.from_agent("cad", "NOT READY: Need dimensions (width, height) and material selection."),
24
  ]
25
  result = analyze_gaps(responses)
26
  assert result.has_gaps
 
30
 
31
  def test_detects_not_ready_from_cnc(self):
32
  responses = [
33
+ AgentResponse.from_agent("cnc", "NOT READY: Need material and tolerance info for manufacturability check."),
34
  ]
35
  result = analyze_gaps(responses)
36
  assert result.has_gaps
 
40
 
41
  def test_detects_not_ready_from_cam(self):
42
  responses = [
43
+ AgentResponse.from_agent("cam", "NOT READY: No 3D model available. Need CAD generation first."),
44
  ]
45
  result = analyze_gaps(responses)
46
  assert result.has_gaps
 
49
 
50
  def test_deduplicates_across_agents(self):
51
  responses = [
52
+ AgentResponse.from_agent("cad", "NOT READY: Need material and dimensions."),
53
+ AgentResponse.from_agent("cnc", "NOT READY: Material is required for manufacturability."),
54
  ]
55
  result = analyze_gaps(responses)
56
  material_items = [i for i in result.missing_items if i.category == "material"]
 
59
 
60
  def test_case_insensitive_not_ready(self):
61
  responses = [
62
+ AgentResponse.from_agent("cad", "not ready: missing width and height dimensions."),
63
  ]
64
  result = analyze_gaps(responses)
65
  assert result.has_gaps
 
67
 
68
  def test_no_false_positive_on_regular_message(self):
69
  responses = [
70
+ AgentResponse.from_agent("cad", "Model generated successfully."),
71
+ AgentResponse.from_agent("cnc", "3-axis machining is ready for this part."),
72
  ]
73
  result = analyze_gaps(responses)
74
  assert not result.has_gaps