Spaces:
Sleeping
Sleeping
Commit ·
98e04b1
1
Parent(s): 6d4f152
refactor: update CrewOrchestrator to return ChatTurnResponse
Browse filesCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- agents/crew_orchestrator.py +46 -64
- agents/gap_analyzer.py +5 -1
- tests/test_crew_orchestrator.py +35 -70
agents/crew_orchestrator.py
CHANGED
|
@@ -15,7 +15,7 @@ from agents.base import BaseOrchestrator
|
|
| 15 |
from core.utils import derive_part_name
|
| 16 |
from agents.design_state import DesignState, DesignPlan, extract_decisions, compute_score
|
| 17 |
from agents.gap_analyzer import analyze_gaps, generate_question_cards
|
| 18 |
-
from agents.agent_flow import AgentResponse
|
| 19 |
from config.settings import settings
|
| 20 |
|
| 21 |
logger = logging.getLogger(__name__)
|
|
@@ -109,44 +109,39 @@ class CrewOrchestrator(BaseOrchestrator):
|
|
| 109 |
history: list[dict],
|
| 110 |
mentions: list[str] | None = None,
|
| 111 |
max_history: int = 30,
|
| 112 |
-
design_state:
|
| 113 |
plan_context: bool = False,
|
| 114 |
-
) ->
|
|
|
|
|
|
|
|
|
|
| 115 |
# Phase: manual plan trigger (before crew/fallback dispatch)
|
| 116 |
-
state = DesignState(**(design_state or {}))
|
| 117 |
if state.phase == "exploring" and _is_plan_trigger(message):
|
| 118 |
score = compute_score(state)
|
| 119 |
plan = DesignPlan.from_state(state, confidence_score=score)
|
| 120 |
state.phase = "planning"
|
| 121 |
state.plan = plan
|
| 122 |
-
return
|
| 123 |
-
"responses": [],
|
| 124 |
-
"preview": None,
|
| 125 |
-
"design_state": state.model_dump(),
|
| 126 |
-
"question_cards": [],
|
| 127 |
-
}
|
| 128 |
|
| 129 |
if not self._crew_available:
|
| 130 |
-
return self._fallback(message, history, mentions, max_history,
|
| 131 |
|
| 132 |
try:
|
| 133 |
-
return self._run_crew(message, history, mentions, max_history,
|
| 134 |
except Exception as exc:
|
| 135 |
logger.warning("CrewAI run failed (%s), falling back", exc, exc_info=True)
|
| 136 |
try:
|
| 137 |
-
return self._fallback(message, history, mentions, max_history,
|
| 138 |
except Exception as fallback_exc:
|
| 139 |
logger.error("Fallback also failed: %s", fallback_exc, exc_info=True)
|
| 140 |
-
return
|
| 141 |
-
|
| 142 |
"design",
|
| 143 |
f"Backend error: {exc}. Fallback also failed: {fallback_exc}. "
|
| 144 |
f"Please check that your API key is set correctly.",
|
| 145 |
-
)
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
"question_cards": [],
|
| 149 |
-
}
|
| 150 |
|
| 151 |
def _run_crew(
|
| 152 |
self,
|
|
@@ -154,13 +149,13 @@ class CrewOrchestrator(BaseOrchestrator):
|
|
| 154 |
history: list[dict],
|
| 155 |
mentions: list[str] | None,
|
| 156 |
max_history: int,
|
| 157 |
-
|
| 158 |
plan_context: bool = False,
|
| 159 |
-
) ->
|
| 160 |
from agents.agent_flow import AgentFlowState, AgentDispatchFlow, collect_responses
|
| 161 |
from agents.tools import set_design_state
|
| 162 |
|
| 163 |
-
state = DesignState(**(
|
| 164 |
|
| 165 |
# Phase: if in planning and user sends a non-plan message, reset to exploring.
|
| 166 |
# When plan_context=True the message is a plan-field query (Ask Agent),
|
|
@@ -204,8 +199,6 @@ class CrewOrchestrator(BaseOrchestrator):
|
|
| 204 |
cad_code = flow.state.cad_code
|
| 205 |
cam_plan = flow.state.cam_plan
|
| 206 |
|
| 207 |
-
responses = [r.model_dump() for r in agent_responses]
|
| 208 |
-
|
| 209 |
# ── Post-processing ─────────────────────────────────────────────
|
| 210 |
preview = None
|
| 211 |
|
|
@@ -236,18 +229,18 @@ class CrewOrchestrator(BaseOrchestrator):
|
|
| 236 |
pass
|
| 237 |
|
| 238 |
validation = validate_for_cnc(shape, part_name=part_name)
|
| 239 |
-
preview =
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
|
| 249 |
# G-code generation
|
| 250 |
-
if preview and preview.
|
| 251 |
from core.cam import generate_gcode
|
| 252 |
from agents.tools import get_last_shape
|
| 253 |
|
|
@@ -259,12 +252,11 @@ class CrewOrchestrator(BaseOrchestrator):
|
|
| 259 |
tool_config=cam_plan.to_tool_config(),
|
| 260 |
post_processor=cam_plan.post_processor,
|
| 261 |
)
|
| 262 |
-
preview
|
| 263 |
if cam_result.success and cam_result.gcode:
|
| 264 |
-
|
| 265 |
-
gcode_path = self.output_dir / f"{part_name}.gcode"
|
| 266 |
gcode_path.write_text(cam_result.gcode)
|
| 267 |
-
preview
|
| 268 |
|
| 269 |
# Update design state
|
| 270 |
updated_state = extract_decisions(agent_responses, state, message)
|
|
@@ -273,8 +265,7 @@ class CrewOrchestrator(BaseOrchestrator):
|
|
| 273 |
gap_result = analyze_gaps(agent_responses)
|
| 274 |
question_cards = []
|
| 275 |
if gap_result.has_gaps:
|
| 276 |
-
|
| 277 |
-
question_cards = [c.model_dump() for c in cards]
|
| 278 |
|
| 279 |
# Auto-trigger plan if score crosses threshold
|
| 280 |
if updated_state.phase == "exploring":
|
|
@@ -292,12 +283,12 @@ class CrewOrchestrator(BaseOrchestrator):
|
|
| 292 |
updated_state.plan = None
|
| 293 |
break
|
| 294 |
|
| 295 |
-
return
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
|
| 302 |
def _fallback(
|
| 303 |
self,
|
|
@@ -305,30 +296,21 @@ class CrewOrchestrator(BaseOrchestrator):
|
|
| 305 |
history: list[dict],
|
| 306 |
mentions: list[str] | None,
|
| 307 |
max_history: int,
|
| 308 |
-
design_state:
|
| 309 |
plan_context: bool = False,
|
| 310 |
-
) ->
|
| 311 |
"""Fall back to MockChatBackend."""
|
| 312 |
from agents.tools import set_design_state
|
| 313 |
from agents.orchestrator import MockChatBackend
|
| 314 |
|
| 315 |
-
|
| 316 |
-
# from user message so gap analysis reflects current turn data.
|
| 317 |
-
state = DesignState(**(design_state or {}))
|
| 318 |
state = state.update_from_messages([], user_message=message)
|
| 319 |
set_design_state(state)
|
| 320 |
|
| 321 |
mock = MockChatBackend(output_dir=self.output_dir)
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
if gap_result.has_gaps:
|
| 328 |
-
cards = generate_question_cards(gap_result, state, user_message=message)
|
| 329 |
-
question_cards = [c.model_dump() for c in cards]
|
| 330 |
-
|
| 331 |
-
# Convert to dict for CrewOrchestrator's current dict-based return
|
| 332 |
-
result: dict = turn.model_dump()
|
| 333 |
-
result["question_cards"] = question_cards
|
| 334 |
return result
|
|
|
|
| 15 |
from core.utils import derive_part_name
|
| 16 |
from agents.design_state import DesignState, DesignPlan, extract_decisions, compute_score
|
| 17 |
from agents.gap_analyzer import analyze_gaps, generate_question_cards
|
| 18 |
+
from agents.agent_flow import AgentResponse, ChatTurnResponse, PreviewData
|
| 19 |
from config.settings import settings
|
| 20 |
|
| 21 |
logger = logging.getLogger(__name__)
|
|
|
|
| 109 |
history: list[dict],
|
| 110 |
mentions: list[str] | None = None,
|
| 111 |
max_history: int = 30,
|
| 112 |
+
design_state: DesignState | None = None,
|
| 113 |
plan_context: bool = False,
|
| 114 |
+
) -> ChatTurnResponse:
|
| 115 |
+
# Backward compat: accept dict during migration
|
| 116 |
+
state = design_state if isinstance(design_state, DesignState) else DesignState(**(design_state or {}))
|
| 117 |
+
|
| 118 |
# Phase: manual plan trigger (before crew/fallback dispatch)
|
|
|
|
| 119 |
if state.phase == "exploring" and _is_plan_trigger(message):
|
| 120 |
score = compute_score(state)
|
| 121 |
plan = DesignPlan.from_state(state, confidence_score=score)
|
| 122 |
state.phase = "planning"
|
| 123 |
state.plan = plan
|
| 124 |
+
return ChatTurnResponse(design_state=state)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
if not self._crew_available:
|
| 127 |
+
return self._fallback(message, history, mentions, max_history, state, plan_context)
|
| 128 |
|
| 129 |
try:
|
| 130 |
+
return self._run_crew(message, history, mentions, max_history, state, plan_context)
|
| 131 |
except Exception as exc:
|
| 132 |
logger.warning("CrewAI run failed (%s), falling back", exc, exc_info=True)
|
| 133 |
try:
|
| 134 |
+
return self._fallback(message, history, mentions, max_history, state, plan_context)
|
| 135 |
except Exception as fallback_exc:
|
| 136 |
logger.error("Fallback also failed: %s", fallback_exc, exc_info=True)
|
| 137 |
+
return ChatTurnResponse(
|
| 138 |
+
responses=[AgentResponse.from_agent(
|
| 139 |
"design",
|
| 140 |
f"Backend error: {exc}. Fallback also failed: {fallback_exc}. "
|
| 141 |
f"Please check that your API key is set correctly.",
|
| 142 |
+
)],
|
| 143 |
+
design_state=state,
|
| 144 |
+
)
|
|
|
|
|
|
|
| 145 |
|
| 146 |
def _run_crew(
|
| 147 |
self,
|
|
|
|
| 149 |
history: list[dict],
|
| 150 |
mentions: list[str] | None,
|
| 151 |
max_history: int,
|
| 152 |
+
design_state: DesignState | None,
|
| 153 |
plan_context: bool = False,
|
| 154 |
+
) -> ChatTurnResponse:
|
| 155 |
from agents.agent_flow import AgentFlowState, AgentDispatchFlow, collect_responses
|
| 156 |
from agents.tools import set_design_state
|
| 157 |
|
| 158 |
+
state = design_state if isinstance(design_state, DesignState) else DesignState(**(design_state or {}))
|
| 159 |
|
| 160 |
# Phase: if in planning and user sends a non-plan message, reset to exploring.
|
| 161 |
# When plan_context=True the message is a plan-field query (Ask Agent),
|
|
|
|
| 199 |
cad_code = flow.state.cad_code
|
| 200 |
cam_plan = flow.state.cam_plan
|
| 201 |
|
|
|
|
|
|
|
| 202 |
# ── Post-processing ─────────────────────────────────────────────
|
| 203 |
preview = None
|
| 204 |
|
|
|
|
| 229 |
pass
|
| 230 |
|
| 231 |
validation = validate_for_cnc(shape, part_name=part_name)
|
| 232 |
+
preview = PreviewData(
|
| 233 |
+
success=True,
|
| 234 |
+
part_name=part_name,
|
| 235 |
+
stl_url=f"/api/models/{part_name}.stl",
|
| 236 |
+
step_url=f"/api/models/{part_name}.step",
|
| 237 |
+
threemf_url=f"/api/models/{part_name}.3mf",
|
| 238 |
+
execution=execution_data,
|
| 239 |
+
validation=validation.model_dump(),
|
| 240 |
+
)
|
| 241 |
|
| 242 |
# G-code generation
|
| 243 |
+
if preview and preview.success and cam_plan:
|
| 244 |
from core.cam import generate_gcode
|
| 245 |
from agents.tools import get_last_shape
|
| 246 |
|
|
|
|
| 252 |
tool_config=cam_plan.to_tool_config(),
|
| 253 |
post_processor=cam_plan.post_processor,
|
| 254 |
)
|
| 255 |
+
preview.cam = cam_result.model_dump()
|
| 256 |
if cam_result.success and cam_result.gcode:
|
| 257 |
+
gcode_path = self.output_dir / f"{preview.part_name}.gcode"
|
|
|
|
| 258 |
gcode_path.write_text(cam_result.gcode)
|
| 259 |
+
preview.gcode_url = f"/api/models/{preview.part_name}.gcode"
|
| 260 |
|
| 261 |
# Update design state
|
| 262 |
updated_state = extract_decisions(agent_responses, state, message)
|
|
|
|
| 265 |
gap_result = analyze_gaps(agent_responses)
|
| 266 |
question_cards = []
|
| 267 |
if gap_result.has_gaps:
|
| 268 |
+
question_cards = generate_question_cards(gap_result, updated_state, user_message=message)
|
|
|
|
| 269 |
|
| 270 |
# Auto-trigger plan if score crosses threshold
|
| 271 |
if updated_state.phase == "exploring":
|
|
|
|
| 283 |
updated_state.plan = None
|
| 284 |
break
|
| 285 |
|
| 286 |
+
return ChatTurnResponse(
|
| 287 |
+
responses=agent_responses,
|
| 288 |
+
preview=preview,
|
| 289 |
+
design_state=updated_state,
|
| 290 |
+
question_cards=question_cards,
|
| 291 |
+
)
|
| 292 |
|
| 293 |
def _fallback(
|
| 294 |
self,
|
|
|
|
| 296 |
history: list[dict],
|
| 297 |
mentions: list[str] | None,
|
| 298 |
max_history: int,
|
| 299 |
+
design_state: DesignState | None,
|
| 300 |
plan_context: bool = False,
|
| 301 |
+
) -> ChatTurnResponse:
|
| 302 |
"""Fall back to MockChatBackend."""
|
| 303 |
from agents.tools import set_design_state
|
| 304 |
from agents.orchestrator import MockChatBackend
|
| 305 |
|
| 306 |
+
state = design_state if isinstance(design_state, DesignState) else DesignState(**(design_state or {}))
|
|
|
|
|
|
|
| 307 |
state = state.update_from_messages([], user_message=message)
|
| 308 |
set_design_state(state)
|
| 309 |
|
| 310 |
mock = MockChatBackend(output_dir=self.output_dir)
|
| 311 |
+
result = mock.chat_turn(message, history, mentions, design_state=state, plan_context=plan_context)
|
| 312 |
+
if not result.question_cards:
|
| 313 |
+
gap_result = analyze_gaps(result.responses)
|
| 314 |
+
if gap_result.has_gaps:
|
| 315 |
+
result.question_cards = generate_question_cards(gap_result, state, user_message=message)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
return result
|
agents/gap_analyzer.py
CHANGED
|
@@ -6,11 +6,15 @@ and generates structured question cards for the UI to present to the user.
|
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
|
|
|
|
|
|
| 9 |
from pydantic import BaseModel, Field
|
| 10 |
|
| 11 |
from config.settings import settings
|
| 12 |
from agents.definitions import AGENTS
|
| 13 |
-
|
|
|
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
# ── Models ────────────────────────────────────────────────────────────────────
|
|
|
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
+
from typing import TYPE_CHECKING
|
| 10 |
+
|
| 11 |
from pydantic import BaseModel, Field
|
| 12 |
|
| 13 |
from config.settings import settings
|
| 14 |
from agents.definitions import AGENTS
|
| 15 |
+
|
| 16 |
+
if TYPE_CHECKING:
|
| 17 |
+
from agents.agent_flow import AgentResponse
|
| 18 |
|
| 19 |
|
| 20 |
# ── Models ────────────────────────────────────────────────────────────────────
|
tests/test_crew_orchestrator.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
"""Tests for CrewOrchestrator — CrewAI-based multi-agent orchestrator."""
|
| 2 |
|
| 3 |
from agents.crew_orchestrator import CrewOrchestrator, _get_crewai_model, _is_plan_trigger
|
| 4 |
-
from agents.agent_flow import WIKI_DIR
|
|
|
|
| 5 |
|
| 6 |
|
| 7 |
class TestGetCrewaiModel:
|
|
@@ -42,16 +43,14 @@ class TestCrewOrchestratorFallback:
|
|
| 42 |
orch = CrewOrchestrator(backend_name="gemini", output_dir=tmp_output_dir)
|
| 43 |
orch._crew_available = False
|
| 44 |
result = orch.chat_turn("test", history=[])
|
| 45 |
-
assert
|
| 46 |
-
assert "preview" in result
|
| 47 |
-
assert "design_state" in result
|
| 48 |
|
| 49 |
def test_response_format(self, tmp_output_dir):
|
| 50 |
orch = CrewOrchestrator(backend_name="gemini", output_dir=tmp_output_dir)
|
| 51 |
orch._crew_available = False
|
| 52 |
result = orch.chat_turn("I need a bracket", history=[])
|
| 53 |
-
assert isinstance(result
|
| 54 |
-
assert isinstance(result
|
| 55 |
|
| 56 |
|
| 57 |
class TestGetOrchestrator:
|
|
@@ -79,32 +78,24 @@ class TestGetOrchestrator:
|
|
| 79 |
class TestGapAnalysis:
|
| 80 |
def test_not_ready_produces_question_cards(self):
|
| 81 |
orch = CrewOrchestrator(backend_name="mock")
|
| 82 |
-
result = orch.chat_turn(
|
| 83 |
-
|
| 84 |
-
history=[],
|
| 85 |
-
design_state={},
|
| 86 |
-
)
|
| 87 |
-
assert "question_cards" in result
|
| 88 |
|
| 89 |
def test_no_question_cards_when_no_gaps(self):
|
| 90 |
orch = CrewOrchestrator(backend_name="mock")
|
| 91 |
result = orch.chat_turn(
|
| 92 |
-
message="I need a bracket",
|
| 93 |
-
|
| 94 |
-
design_state={"material": "aluminum", "dimensions": {"width": 60}},
|
| 95 |
)
|
| 96 |
-
assert
|
| 97 |
-
assert isinstance(result["question_cards"], list)
|
| 98 |
|
| 99 |
def test_plan_trigger_includes_question_cards_key(self):
|
| 100 |
orch = CrewOrchestrator(backend_name="mock")
|
| 101 |
result = orch.chat_turn(
|
| 102 |
-
message="show plan",
|
| 103 |
-
|
| 104 |
-
design_state={"material": "aluminum"},
|
| 105 |
)
|
| 106 |
-
assert
|
| 107 |
-
assert result["question_cards"] == []
|
| 108 |
|
| 109 |
|
| 110 |
class TestPlanningPhase:
|
|
@@ -113,62 +104,36 @@ class TestPlanningPhase:
|
|
| 113 |
def test_manual_plan_trigger(self):
|
| 114 |
"""User typing a trigger keyword returns plan without running crew."""
|
| 115 |
orch = CrewOrchestrator(backend_name="mock")
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
"
|
| 119 |
-
"
|
| 120 |
-
"axis_recommendation": "3-axis",
|
| 121 |
-
}
|
| 122 |
-
result = orch.chat_turn(
|
| 123 |
-
message="show plan",
|
| 124 |
-
history=[],
|
| 125 |
-
design_state=state_dict,
|
| 126 |
)
|
| 127 |
-
|
| 128 |
-
assert result
|
| 129 |
-
assert result
|
|
|
|
| 130 |
|
| 131 |
def test_approved_phase_keeps_approved(self):
|
| 132 |
"""When phase is approved, orchestrator keeps it for agent run."""
|
| 133 |
orch = CrewOrchestrator(backend_name="mock")
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
"confidence_score": 9.0,
|
| 139 |
-
}
|
| 140 |
-
state_dict = {
|
| 141 |
-
"phase": "approved",
|
| 142 |
-
"plan": plan_dict,
|
| 143 |
-
"material": "aluminum",
|
| 144 |
-
"dimensions": {"width": 60},
|
| 145 |
-
}
|
| 146 |
-
result = orch.chat_turn(
|
| 147 |
-
message="Generate the approved design",
|
| 148 |
-
history=[],
|
| 149 |
-
design_state=state_dict,
|
| 150 |
)
|
| 151 |
-
|
|
|
|
|
|
|
| 152 |
|
| 153 |
def test_planning_phase_resets_on_message(self):
|
| 154 |
"""If phase is planning and user sends regular message, reset to exploring."""
|
| 155 |
orch = CrewOrchestrator(backend_name="mock")
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
"confidence_score": 5.0,
|
| 161 |
-
}
|
| 162 |
-
state_dict = {
|
| 163 |
-
"phase": "planning",
|
| 164 |
-
"plan": plan_dict,
|
| 165 |
-
"material": "steel",
|
| 166 |
-
}
|
| 167 |
-
result = orch.chat_turn(
|
| 168 |
-
message="actually change the material",
|
| 169 |
-
history=[],
|
| 170 |
-
design_state=state_dict,
|
| 171 |
)
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
assert
|
|
|
|
| 1 |
"""Tests for CrewOrchestrator — CrewAI-based multi-agent orchestrator."""
|
| 2 |
|
| 3 |
from agents.crew_orchestrator import CrewOrchestrator, _get_crewai_model, _is_plan_trigger
|
| 4 |
+
from agents.agent_flow import ChatTurnResponse, PreviewData, WIKI_DIR
|
| 5 |
+
from agents.design_state import DesignState, DesignPlan
|
| 6 |
|
| 7 |
|
| 8 |
class TestGetCrewaiModel:
|
|
|
|
| 43 |
orch = CrewOrchestrator(backend_name="gemini", output_dir=tmp_output_dir)
|
| 44 |
orch._crew_available = False
|
| 45 |
result = orch.chat_turn("test", history=[])
|
| 46 |
+
assert isinstance(result, ChatTurnResponse)
|
|
|
|
|
|
|
| 47 |
|
| 48 |
def test_response_format(self, tmp_output_dir):
|
| 49 |
orch = CrewOrchestrator(backend_name="gemini", output_dir=tmp_output_dir)
|
| 50 |
orch._crew_available = False
|
| 51 |
result = orch.chat_turn("I need a bracket", history=[])
|
| 52 |
+
assert isinstance(result.responses, list)
|
| 53 |
+
assert isinstance(result.design_state, DesignState)
|
| 54 |
|
| 55 |
|
| 56 |
class TestGetOrchestrator:
|
|
|
|
| 78 |
class TestGapAnalysis:
|
| 79 |
def test_not_ready_produces_question_cards(self):
|
| 80 |
orch = CrewOrchestrator(backend_name="mock")
|
| 81 |
+
result = orch.chat_turn(message="generate a bracket", history=[])
|
| 82 |
+
assert isinstance(result.question_cards, list)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
def test_no_question_cards_when_no_gaps(self):
|
| 85 |
orch = CrewOrchestrator(backend_name="mock")
|
| 86 |
result = orch.chat_turn(
|
| 87 |
+
message="I need a bracket", history=[],
|
| 88 |
+
design_state=DesignState(material="aluminum", dimensions={"width": 60}),
|
|
|
|
| 89 |
)
|
| 90 |
+
assert isinstance(result.question_cards, list)
|
|
|
|
| 91 |
|
| 92 |
def test_plan_trigger_includes_question_cards_key(self):
|
| 93 |
orch = CrewOrchestrator(backend_name="mock")
|
| 94 |
result = orch.chat_turn(
|
| 95 |
+
message="show plan", history=[],
|
| 96 |
+
design_state=DesignState(material="aluminum"),
|
|
|
|
| 97 |
)
|
| 98 |
+
assert result.question_cards == []
|
|
|
|
| 99 |
|
| 100 |
|
| 101 |
class TestPlanningPhase:
|
|
|
|
| 104 |
def test_manual_plan_trigger(self):
|
| 105 |
"""User typing a trigger keyword returns plan without running crew."""
|
| 106 |
orch = CrewOrchestrator(backend_name="mock")
|
| 107 |
+
state = DesignState(
|
| 108 |
+
part_name="bracket", material="aluminum 6061",
|
| 109 |
+
dimensions={"width": 60, "height": 40, "depth": 20},
|
| 110 |
+
axis_recommendation="3-axis",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
)
|
| 112 |
+
result = orch.chat_turn(message="show plan", history=[], design_state=state)
|
| 113 |
+
assert result.design_state.phase == "planning"
|
| 114 |
+
assert result.design_state.plan is not None
|
| 115 |
+
assert result.design_state.plan.material == "aluminum 6061"
|
| 116 |
|
| 117 |
def test_approved_phase_keeps_approved(self):
|
| 118 |
"""When phase is approved, orchestrator keeps it for agent run."""
|
| 119 |
orch = CrewOrchestrator(backend_name="mock")
|
| 120 |
+
plan = DesignPlan(
|
| 121 |
+
part_name="bracket", description="test", material="aluminum",
|
| 122 |
+
dimensions={"width": 60}, features=[], constraints=[],
|
| 123 |
+
axis_recommendation="3-axis", machining_notes=[], confidence_score=9.0,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
)
|
| 125 |
+
state = DesignState(phase="approved", plan=plan, material="aluminum", dimensions={"width": 60})
|
| 126 |
+
result = orch.chat_turn(message="Generate the approved design", history=[], design_state=state)
|
| 127 |
+
assert isinstance(result.responses, list)
|
| 128 |
|
| 129 |
def test_planning_phase_resets_on_message(self):
|
| 130 |
"""If phase is planning and user sends regular message, reset to exploring."""
|
| 131 |
orch = CrewOrchestrator(backend_name="mock")
|
| 132 |
+
plan = DesignPlan(
|
| 133 |
+
part_name="bracket", description="", material="steel",
|
| 134 |
+
dimensions={}, features=[], constraints=[],
|
| 135 |
+
axis_recommendation="", machining_notes=[], confidence_score=5.0,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
)
|
| 137 |
+
state = DesignState(phase="planning", plan=plan, material="steel")
|
| 138 |
+
result = orch.chat_turn(message="actually change the material", history=[], design_state=state)
|
| 139 |
+
assert result.design_state.phase in ("exploring", "planning")
|