from __future__ import annotations import asyncio import json import sys import tempfile import unittest from pathlib import Path from unittest.mock import patch ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "backend")) from app.models.contracts import ChatMessage, MessagePart # noqa: E402 from app.services.agent_service import AgentService # noqa: E402 from app.services.engine_service import load_engine # noqa: E402 from app.services.flange_sleeve_template import TEMPLATE_ID # noqa: E402 from app.services.library import CdslLibrary # noqa: E402 from app.services.part_skills import PartSkillLibrary # noqa: E402 from app.services.storage import WorkspaceStore # noqa: E402 from app.settings import ProviderConfig, ProviderModel, Settings # noqa: E402 from tests.test_design_intent import mounting_plate_cdsl # noqa: E402 BACKEND = ROOT / "backend" PART_SKILL_ROOT = BACKEND / "agent" / "skills" / "cad-engine" / "references" / "part-skills" class DesignIntentFlowTests(unittest.TestCase): def settings(self, root: Path) -> Settings: provider = ProviderConfig("test", "Test", "https://example.invalid/v1", "test-key", (ProviderModel("test-model"),)) return Settings( task_root=root / "tasks", conversation_root=root / "conversations", library_root=BACKEND / "cdsl_library", engine_root=BACKEND / "engine" / "cdsl_engine", llm_base_url=provider.base_url, llm_api_key=provider.api_key, llm_model="test-model", llm_timeout_s=1, default_provider_id="test", providers=(provider,), ) def test_design_brief_is_required_before_library_or_cdsl(self) -> None: with tempfile.TemporaryDirectory() as directory: settings = self.settings(Path(directory)) store = WorkspaceStore(settings) agent = AgentService(settings, store, CdslLibrary(settings), PartSkillLibrary(PART_SKILL_ROOT)) state = {"phase": "WAITING_FOR_PLAN", "design_brief": ""} searched, _ = asyncio.run(agent._run_tool("search_cdsl_library", {"query": "mounting plate"}, "", "mounting plate", [], planning_state=state)) generated, _ = asyncio.run(agent._run_tool("generate_cdsl_model", {"cdsl": {}, "summary": "x", "assumptions": []}, "", "mounting plate", [], planning_state=state)) self.assertEqual(searched["code"], "DESIGN_BRIEF_REQUIRED") self.assertEqual(generated["code"], "DESIGN_BRIEF_REQUIRED") self.assertEqual(list(settings.task_root.glob("cad_*")), []) def test_generation_normalizes_legacy_llm_cdsl_before_building(self) -> None: legacy_cdsl = { "schema": "cad.cdsl.llm.v1", "part_id": "legacy-flange-base", "geometry": {"sketches": [{ "id": "base_sketch", "plane": "XY", "offset_mm": 12, "profile": {"type": "circle", "radius_mm": 20}, }]}, "features": [{ "id": "base_add", "atomic_id": "extrude_add_blind", "sketch": "base_sketch", "params": {"distance_mm": 8}, }], } with tempfile.TemporaryDirectory() as directory: settings = self.settings(Path(directory)) store = WorkspaceStore(settings) agent = AgentService(settings, store, CdslLibrary(settings), PartSkillLibrary(PART_SKILL_ROOT)) state = {"phase": "PLAN_RECORDED", "design_brief": "Create a flange base."} captured_build: dict[str, object] = {} def fake_build_revision(**kwargs: object) -> dict[str, object]: captured_build.update(kwargs) return {"task_id": "cad_aaaaaaaaaaaa", "revision_id": "rev_001"} with patch("app.services.agent_service.build_revision", side_effect=fake_build_revision): result, _ = asyncio.run(agent._run_tool( "generate_cdsl_model", {"cdsl": legacy_cdsl, "summary": "flange base", "assumptions": []}, "", "Create a flange base", [], planning_state=state, )) built_cdsl = captured_build["cdsl"] self.assertTrue(result["ok"]) self.assertEqual(built_cdsl["features"][0]["sketch_id"], "base_sketch") self.assertEqual(built_cdsl["features"][0]["depends_on"], []) self.assertNotIn("sketch", built_cdsl["features"][0]) self.assertIn("workplane", built_cdsl["geometry"]["sketches"][0]) self.assertEqual(len(result["normalization_repairs"]), 3) def test_flange_sleeve_tool_builds_cdsl_from_a_compact_semantic_plan(self) -> None: with tempfile.TemporaryDirectory() as directory: settings = self.settings(Path(directory)) store = WorkspaceStore(settings) agent = AgentService(settings, store, CdslLibrary(settings), PartSkillLibrary(PART_SKILL_ROOT)) state = {"phase": "PLAN_RECORDED", "design_brief": "Create a flange sleeve."} captured_build: dict[str, object] = {} def fake_build_revision(**kwargs: object) -> dict[str, object]: captured_build.update(kwargs) return {"task_id": "cad_aaaaaaaaaaaa", "revision_id": "rev_001"} with patch("app.services.agent_service.build_revision", side_effect=fake_build_revision): result, _ = asyncio.run(agent._run_tool( "generate_flange_sleeve_model", { "plan": { "template": TEMPLATE_ID, "name": "Image flange sleeve", "flange_width_mm": 140, "flange_height_mm": 120, "mount_hole_u_mm": 48, "mount_hole_v_mm": 38, }, "summary": "flange sleeve", "assumptions": ["Dimensions are approximate."], }, "", "Create an image flange sleeve", [], planning_state=state, )) built_cdsl = captured_build["cdsl"] self.assertTrue(result["ok"]) self.assertEqual(result["template_id"], TEMPLATE_ID) self.assertEqual(captured_build["operation"]["template_id"], TEMPLATE_ID) self.assertEqual(built_cdsl["meta"]["name"], "Image flange sleeve") self.assertEqual(built_cdsl["geometry"]["sketches"][0]["workplane"]["normal"], [1.0, 0.0, 0.0]) self.assertEqual(built_cdsl["features"][-1]["depends_on"], ["mount_hole_cuts"]) self.assertNotIn("cdsl", result["template_plan"]) def test_successful_generation_ends_the_agent_tool_loop(self) -> None: class CaptureAgent(AgentService): def __init__(self, *args: object, **kwargs: object) -> None: super().__init__(*args, **kwargs) self.responses = [ { "choices": [{"message": { "role": "assistant", "content": "", "tool_calls": [{ "id": "brief", "type": "function", "function": { "name": "describe_design_intent", "arguments": json.dumps({"plan": "Create a flange sleeve.", "assumptions": []}), }, }], }}], }, { "choices": [{"message": { "role": "assistant", "content": "", "tool_calls": [{ "id": "template", "type": "function", "function": { "name": "generate_flange_sleeve_model", "arguments": json.dumps({ "plan": {"template": TEMPLATE_ID}, "summary": "flange sleeve", "assumptions": [], }), }, }], }}], }, ] async def _complete(self, *args: object, **kwargs: object) -> dict[str, object]: return self.responses.pop(0) with tempfile.TemporaryDirectory() as directory: settings = self.settings(Path(directory)) store = WorkspaceStore(settings) agent = CaptureAgent(settings, store, CdslLibrary(settings), PartSkillLibrary(PART_SKILL_ROOT)) def fake_build_revision(**kwargs: object) -> dict[str, object]: return { "task_id": "cad_aaaaaaaaaaaa", "revision_id": "rev_001", "cdsl_path": "revisions/rev_001/model.cdsl.json", "step_path": "revisions/rev_001/model.step", "glb_path": "revisions/rev_001/model.glb", "report_path": "revisions/rev_001/rebuild-report.json", "summary": str(kwargs["summary"]), "reference_ids": [], "engine": "cdsl_only", } with patch("app.services.agent_service.build_revision", side_effect=fake_build_revision): async def consume() -> None: message = ChatMessage(id="user_1", role="user", parts=[MessagePart(type="text", text="Create a flange sleeve")]) async for _ in agent.stream([message], None, None): pass asyncio.run(consume()) saved = store.read_conversation(next(item.name for item in settings.conversation_root.iterdir())) parts = saved["messages"][-1]["parts"] self.assertEqual(agent.responses, []) self.assertTrue(any(part["type"] == "data-cad-result" for part in parts)) self.assertFalse(any(part["type"] == "data-cad-error" for part in parts)) def test_text_brief_is_returned_to_model_and_cdsl_is_the_only_contract(self) -> None: class CaptureAgent(AgentService): def __init__(self, *args: object, **kwargs: object) -> None: super().__init__(*args, **kwargs) self.seen_messages: list[list[dict[str, object]]] = [] self.responses = [ { "choices": [{"message": { "role": "assistant", "content": "", "tool_calls": [{ "id": "brief", "type": "function", "function": { "name": "describe_design_intent", "arguments": json.dumps({ "plan": "Create a rectangular mounting plate, then cut four mounting holes and a center slot.", "assumptions": ["Use millimetres."], }), }, }], }}], }, { "choices": [{"message": { "role": "assistant", "content": "", "tool_calls": [{ "id": "generate", "type": "function", "function": { "name": "generate_cdsl_model", "arguments": json.dumps({ "cdsl": mounting_plate_cdsl(), "summary": "mounting plate", "assumptions": ["Use millimetres."], }), }, }], }}], }, {"choices": [{"message": {"role": "assistant", "content": "已生成。", "tool_calls": []}}]}, ] async def _complete(self, messages: list[dict[str, object]], *args: object, **kwargs: object) -> dict[str, object]: self.seen_messages.append([dict(message) for message in messages]) return self.responses.pop(0) with tempfile.TemporaryDirectory() as directory: settings = self.settings(Path(directory)) store = WorkspaceStore(settings) skills = PartSkillLibrary(PART_SKILL_ROOT) agent = CaptureAgent(settings, store, CdslLibrary(settings), skills) captured_build: dict[str, object] = {} def fake_build_revision(**kwargs: object) -> dict[str, object]: captured_build.update(kwargs) return { "task_id": "cad_aaaaaaaaaaaa", "revision_id": "rev_001", "cdsl_path": "revisions/rev_001/model.cdsl.json", "step_path": "revisions/rev_001/model.step", "glb_path": "revisions/rev_001/model.glb", "report_path": "revisions/rev_001/rebuild-report.json", "summary": str(kwargs["summary"]), "reference_ids": list(kwargs["reference_ids"]), "engine": "cdsl_only", } engine = load_engine(settings) with patch.object(engine, "validate_intent_cdsl", side_effect=AssertionError("legacy intent validation must not run")) as legacy_validation, patch("app.services.agent_service.build_revision", side_effect=fake_build_revision): async def consume() -> None: message = ChatMessage(id="user_1", role="user", parts=[MessagePart(type="text", text="Create a mounting plate")]) async for _ in agent.stream([message], None, None): pass asyncio.run(consume()) brief_result = json.loads(str(agent.seen_messages[1][-1]["content"])) self.assertEqual(brief_result["plan"], "Create a rectangular mounting plate, then cut four mounting holes and a center slot.") self.assertNotIn("design_intent_id", brief_result) self.assertNotIn("design_intent_id", captured_build) self.assertNotIn("design_intent", captured_build) self.assertEqual(captured_build["parent_revision_id"], "") legacy_validation.assert_not_called() def test_revision_parent_is_taken_from_the_current_successful_cdsl_revision(self) -> None: with tempfile.TemporaryDirectory() as directory: settings = self.settings(Path(directory)) store = WorkspaceStore(settings) agent = AgentService(settings, store, CdslLibrary(settings), PartSkillLibrary(PART_SKILL_ROOT)) state = {"phase": "WAITING_FOR_PLAN", "design_brief": ""} asyncio.run(agent._run_tool( "describe_design_intent", {"plan": "Increase the plate thickness and preserve the existing hole layout.", "assumptions": []}, "cad_aaaaaaaaaaaa", "Revise the plate", [], planning_state=state, )) store.read_task = lambda _task_id: {"current_revision": "rev_007"} # type: ignore[method-assign] captured_build: dict[str, object] = {} def fake_build_revision(**kwargs: object) -> dict[str, object]: captured_build.update(kwargs) return {"task_id": "cad_aaaaaaaaaaaa", "revision_id": "rev_008"} with patch("app.services.agent_service.build_revision", side_effect=fake_build_revision): result, _ = asyncio.run(agent._run_tool( "generate_cdsl_model", {"cdsl": mounting_plate_cdsl(), "summary": "revised plate", "assumptions": []}, "cad_aaaaaaaaaaaa", "Revise the plate", [], planning_state=state, )) self.assertTrue(result["ok"]) self.assertEqual(captured_build["parent_revision_id"], "rev_007")