CallMeDaniel Claude Opus 4.6 (1M context) commited on
Commit
e65e6f8
·
1 Parent(s): 07c128c

fix: prevent question card loop when agent mentions keywords as context

Browse files

- Add finish category to _is_category_satisfied (checks constraints for
surface finish entries)
- Add anti-loop protection: skip cards for categories whose keywords
appear in the user's current message (they just answered it)
- Pass user_message through to generate_question_cards in both crew and
fallback paths

Fixes: CNC agent mentioning "surface finish" and "Ra" in NOT READY
context text caused the gap analyzer to repeatedly show a finish card
even after the user answered it.

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

agents/crew_orchestrator.py CHANGED
@@ -408,7 +408,7 @@ class CrewOrchestrator(BaseOrchestrator):
408
  gap_result = analyze_gaps(responses)
409
  question_cards = []
410
  if gap_result.has_gaps:
411
- cards = generate_question_cards(gap_result, updated_state)
412
  question_cards = [c.model_dump() for c in cards]
413
 
414
  # Auto-trigger plan if score crosses threshold
@@ -461,7 +461,7 @@ class CrewOrchestrator(BaseOrchestrator):
461
  gap_result = analyze_gaps(result_responses)
462
  if gap_result.has_gaps:
463
  state = DesignState(**(design_state or {}))
464
- cards = generate_question_cards(gap_result, state)
465
  result["question_cards"] = [c.model_dump() for c in cards]
466
  else:
467
  result["question_cards"] = []
 
408
  gap_result = analyze_gaps(responses)
409
  question_cards = []
410
  if gap_result.has_gaps:
411
+ cards = generate_question_cards(gap_result, updated_state, user_message=message)
412
  question_cards = [c.model_dump() for c in cards]
413
 
414
  # Auto-trigger plan if score crosses threshold
 
461
  gap_result = analyze_gaps(result_responses)
462
  if gap_result.has_gaps:
463
  state = DesignState(**(design_state or {}))
464
+ cards = generate_question_cards(gap_result, state, user_message=message)
465
  result["question_cards"] = [c.model_dump() for c in cards]
466
  else:
467
  result["question_cards"] = []
agents/gap_analyzer.py CHANGED
@@ -145,14 +145,37 @@ def _is_category_satisfied(category: str, state) -> bool:
145
  return bool(state.constraints)
146
  if category == "machining":
147
  return bool(state.axis_recommendation)
 
 
148
  return False
149
 
150
 
151
- def generate_question_cards(gaps: GapAnalysis, state) -> list[QuestionCard]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  """Convert a GapAnalysis into question cards, skipping already-satisfied categories.
153
 
154
  Looks up responsible agent from settings, agent metadata from AGENTS dict,
155
  and question text/suggestions from QUESTION_TEMPLATES.
 
 
 
 
 
156
  """
157
  if not gaps.has_gaps:
158
  return []
@@ -167,6 +190,10 @@ def generate_question_cards(gaps: GapAnalysis, state) -> list[QuestionCard]:
167
  if _is_category_satisfied(category, state):
168
  continue
169
 
 
 
 
 
170
  # Look up responsible agent id
171
  responsible_agent = category_agents.get(category, item.agent_id)
172
 
 
145
  return bool(state.constraints)
146
  if category == "machining":
147
  return bool(state.axis_recommendation)
148
+ if category == "finish":
149
+ return any("surface finish" in c.lower() or "ra " in c.lower() for c in (state.constraints or []))
150
  return False
151
 
152
 
153
+ def _is_category_in_user_message(category: str, user_message: str) -> bool:
154
+ """Return True if the user's current message already answers this category.
155
+
156
+ Prevents re-showing cards for categories the user just responded to.
157
+ """
158
+ if not user_message:
159
+ return False
160
+ lower = user_message.lower()
161
+ keywords = settings.gap_analysis.category_keywords.get(category, [])
162
+ return any(kw.lower() in lower for kw in keywords)
163
+
164
+
165
+ def generate_question_cards(
166
+ gaps: GapAnalysis,
167
+ state,
168
+ user_message: str = "",
169
+ ) -> list[QuestionCard]:
170
  """Convert a GapAnalysis into question cards, skipping already-satisfied categories.
171
 
172
  Looks up responsible agent from settings, agent metadata from AGENTS dict,
173
  and question text/suggestions from QUESTION_TEMPLATES.
174
+
175
+ Args:
176
+ gaps: Result of analyze_gaps().
177
+ state: Current DesignState to filter against.
178
+ user_message: The user's current message — skips categories the user just answered.
179
  """
180
  if not gaps.has_gaps:
181
  return []
 
190
  if _is_category_satisfied(category, state):
191
  continue
192
 
193
+ # Skip if the user just answered this category (anti-loop)
194
+ if _is_category_in_user_message(category, user_message):
195
+ continue
196
+
197
  # Look up responsible agent id
198
  responsible_agent = category_agents.get(category, item.agent_id)
199
 
tests/test_gap_analyzer.py CHANGED
@@ -132,6 +132,41 @@ class TestGenerateQuestionCards:
132
  assert cards[0].agent_name == "CNC Agent"
133
  assert cards[0].agent_color == "#00e676"
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  def test_model_category_no_suggestions(self):
136
  gaps = GapAnalysis(
137
  has_gaps=True,
 
132
  assert cards[0].agent_name == "CNC Agent"
133
  assert cards[0].agent_color == "#00e676"
134
 
135
+ def test_finish_satisfied_by_constraint(self):
136
+ gaps = GapAnalysis(
137
+ has_gaps=True,
138
+ missing_items=[
139
+ MissingItem(category="finish", description="surface finish", agent_id="cnc"),
140
+ ],
141
+ )
142
+ state = DesignState(constraints=["surface finish: Standard"])
143
+ cards = generate_question_cards(gaps, state)
144
+ assert len(cards) == 0
145
+
146
+ def test_anti_loop_skips_category_user_just_answered(self):
147
+ gaps = GapAnalysis(
148
+ has_gaps=True,
149
+ missing_items=[
150
+ MissingItem(category="finish", description="surface finish", agent_id="cnc"),
151
+ MissingItem(category="dimension", description="width", agent_id="cad"),
152
+ ],
153
+ )
154
+ cards = generate_question_cards(gaps, DesignState(), user_message="Standard surface finish")
155
+ categories = [c.category for c in cards]
156
+ assert "finish" not in categories
157
+ assert "dimension" in categories
158
+
159
+ def test_anti_loop_no_effect_on_unrelated_message(self):
160
+ gaps = GapAnalysis(
161
+ has_gaps=True,
162
+ missing_items=[
163
+ MissingItem(category="material", description="material", agent_id="cad"),
164
+ ],
165
+ )
166
+ cards = generate_question_cards(gaps, DesignState(), user_message="make it bigger")
167
+ assert len(cards) == 1
168
+ assert cards[0].category == "material"
169
+
170
  def test_model_category_no_suggestions(self):
171
  gaps = GapAnalysis(
172
  has_gaps=True,