Spaces:
Sleeping
Sleeping
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 +2 -2
- agents/gap_analyzer.py +28 -1
- tests/test_gap_analyzer.py +35 -0
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|