优化生成流程

This commit is contained in:
2026-08-25 17:41:24 +08:00
parent 51575161dc
commit beaa5dd9fe
50 changed files with 4204 additions and 4261 deletions
+35 -24
View File
@@ -9,10 +9,12 @@ from types import SimpleNamespace
from unittest.mock import patch
from app.models.contracts import ChatMessage, MessagePart
from app.services.agent_service import AgentService, CDSL_TOOL_SCHEMA, RepeatedToolArgumentsError, StrictToolSchemaError, TOOL_SCHEMAS, ToolArgumentsError, normalize_image_analysis, parse_tool_arguments, response_language_instruction, tools_for_model, use_flange_sleeve_template, user_visible_error_message
from app.services.agent_service import AgentService, CDSL_TOOL_SCHEMA, RepeatedToolArgumentsError, StrictToolSchemaError, TOOL_SCHEMAS, ToolArgumentsError, engine_capability_manifest, normalize_image_analysis, parse_tool_arguments, response_language_instruction, system_prompt, tools_for_model, user_visible_error_message
from app.services.engine_service import load_engine
from app.services.quality import QUALITY_RULE_TYPES
from app.services.library import CdslLibrary
from app.services.storage import WorkspaceStore
from app.settings import ProviderConfig, ProviderModel, Settings
from app.settings import ProviderConfig, ProviderModel, Settings, get_settings
class ToolChoiceCompatibilityTests(unittest.TestCase):
@@ -107,16 +109,31 @@ class ParseToolArgumentsTests(unittest.TestCase):
self.assertNotIn("extrude", cdsl["$defs"]["feature_atomic_ids"]["enum"])
self.assertEqual(cdsl, CDSL_TOOL_SCHEMA)
def test_generation_schema_exposes_only_runtime_atomic_ids(self) -> None:
settings = get_settings()
engine = load_engine(settings)
self.assertEqual(
set(CDSL_TOOL_SCHEMA["$defs"]["feature_atomic_ids"]["enum"]),
set(engine.SUPPORTED_ATOMIC_IDS),
)
def test_verification_schema_exposes_only_implemented_rule_types(self) -> None:
generate_tool = next(tool for tool in TOOL_SCHEMAS if tool["function"]["name"] == "generate_cdsl_model")
verification = generate_tool["function"]["parameters"]["properties"]["verification"]
rule_type = verification["properties"]["rules"]["items"]["properties"]["type"]
self.assertEqual(set(rule_type["enum"]), set(QUALITY_RULE_TYPES))
def test_strict_tool_schema_covers_generation_arguments(self) -> None:
tools = tools_for_model(ProviderModel("strict-model", strict_tool_schema=True))
strict_tools = [tool["function"]["name"] for tool in tools if tool["function"].get("strict")]
self.assertEqual(strict_tools, ["generate_flange_sleeve_model", "generate_cdsl_model"])
template_tool = next(tool for tool in tools if tool["function"]["name"] == "generate_flange_sleeve_model")
self.assertEqual(template_tool["function"]["parameters"]["properties"]["plan"]["required"], ["template"])
self.assertEqual(strict_tools, ["generate_cdsl_model", "patch_cdsl_model"])
generate_tool = next(tool for tool in tools if tool["function"]["name"] == "generate_cdsl_model")
self.assertEqual(generate_tool["function"]["parameters"]["properties"]["summary"], {"type": "string", "minLength": 1})
self.assertEqual(generate_tool["function"]["parameters"]["properties"]["cdsl"], CDSL_TOOL_SCHEMA)
self.assertIn("verification", generate_tool["function"]["parameters"]["properties"])
patch_tool = next(tool for tool in tools if tool["function"]["name"] == "patch_cdsl_model")
self.assertIn("base_revision_id", patch_tool["function"]["parameters"]["required"])
self.assertFalse(generate_tool["function"]["parameters"]["additionalProperties"])
def test_default_model_does_not_receive_strict_tool_schema(self) -> None:
@@ -124,21 +141,14 @@ class ParseToolArgumentsTests(unittest.TestCase):
self.assertFalse(any(tool["function"].get("strict") for tool in tools))
def test_flange_sleeve_image_route_exposes_only_the_compact_plan_generator(self) -> None:
analysis = {
"partType": "带方形法兰的圆筒管座",
"visibleFeatures": ["同轴中空管", "四个安装孔"],
}
self.assertTrue(use_flange_sleeve_template(analysis, ""))
self.assertFalse(use_flange_sleeve_template(analysis, "cad_existing"))
tools = tools_for_model(ProviderModel("default-model"), flange_sleeve_only=True)
names = [tool["function"]["name"] for tool in tools]
self.assertIn("generate_flange_sleeve_model", names)
self.assertNotIn("search_cdsl_library", names)
self.assertNotIn("read_cdsl_reference", names)
self.assertNotIn("read_current_cdsl", names)
self.assertNotIn("generate_cdsl_model", names)
def test_direct_cdsl_tools_exclude_spec_and_template_generators(self) -> None:
names = [tool["function"]["name"] for tool in tools_for_model(ProviderModel("default-model"))]
self.assertIn("generate_cdsl_model", names)
self.assertIn("patch_cdsl_model", names)
self.assertNotIn("create_generation_spec", names)
self.assertNotIn("author_cdsl_from_generation_spec", names)
self.assertNotIn("patch_generation_spec", names)
self.assertNotIn("generate_flange_sleeve_model", names)
def test_recorded_image_analysis_is_not_exposed_as_a_tool(self) -> None:
tools = tools_for_model(ProviderModel("vision-model", vision=True), include_image_analysis=False)
@@ -385,15 +395,15 @@ class ToolArgumentsRetryTests(unittest.TestCase):
diagnostics = settings.conversation_root / conversation_id / "diagnostics"
attempts = sorted(diagnostics.glob("cdsl_attempt_*.json"))
failures = sorted(diagnostics.glob("cdsl_validation_*.json"))
self.assertEqual(len(attempts), 7)
self.assertEqual(len(failures), 7)
self.assertEqual(len(attempts), 5)
self.assertEqual(len(failures), 5)
self.assertTrue(all(json.loads(path.read_text(encoding="utf-8")) == {} for path in attempts))
records = sorted(
(json.loads(path.read_text(encoding="utf-8")) for path in failures),
key=lambda record: int(record["iteration"]),
)
self.assertEqual([record["iteration"] for record in records], list(range(2, 9)))
self.assertEqual([record["iteration"] for record in records], list(range(2, 7)))
self.assertTrue(all(record["kind"] == "cdsl_validation_failure" for record in records))
self.assertTrue(all(record["validation_error_type"] == "ValueError" for record in records))
self.assertTrue(all(record["validation_error"] for record in records))
@@ -402,7 +412,7 @@ class ToolArgumentsRetryTests(unittest.TestCase):
{path.name for path in attempts},
)
self.assertTrue(any("每次 CDSL 校验失败的诊断已保存到" in str(event.get("message", "")) for event in events))
self.assertEqual(agent.responses, [])
self.assertEqual(len(agent.responses), 2)
self.assertEqual(list(settings.task_root.glob("cad_*")), [])
@@ -822,5 +832,6 @@ class StructuredResultResponseTests(unittest.TestCase):
self.assertEqual(json.loads(attempts[0].read_text(encoding="utf-8")), {})
self.assertEqual(list(diagnostics.glob("cdsl_validation_*.json")), [])
if __name__ == "__main__":
unittest.main()
-203
View File
@@ -1,203 +0,0 @@
from __future__ import annotations
import copy
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "backend"))
def workplane(z: float = 0.0) -> dict[str, list[float]]:
return {
"origin_mm": [0.0, 0.0, z],
"x_dir": [1.0, 0.0, 0.0],
"y_dir": [0.0, 1.0, 0.0],
"normal": [0.0, 0.0, 1.0],
}
def mounting_plate_intent() -> dict:
return {
"schema": "cad.cdsl.design-intent.v1",
"schema_version": "1.0",
"mode": "create",
"request": "Create a mounting plate with four holes and a center slot",
"base_revision_id": "",
"structures": [
{
"id": "base_plate",
"cdsl_feature_id": "base_add",
"role": "base",
"purpose": "Rectangular mounting plate",
"depends_on": [],
"cdsl_strategy": {
"atomic_id": "extrude_add_blind",
"profile_type": "rectangle",
"parameter_roles": ["width_mm", "height_mm", "thickness_mm"],
"selector_roles": [],
},
},
{
"id": "mounting_holes",
"cdsl_feature_id": "hole_cut",
"role": "subtractive",
"purpose": "Four mounting holes",
"depends_on": ["base_plate"],
"cdsl_strategy": {
"atomic_id": "extrude_cut_blind",
"profile_type": "circle_grid",
"parameter_roles": ["diameter_mm", "count_x", "count_y"],
"selector_roles": [],
},
},
{
"id": "adjustment_slot",
"cdsl_feature_id": "slot_cut",
"role": "subtractive",
"purpose": "Center adjustment slot",
"depends_on": ["mounting_holes"],
"cdsl_strategy": {
"atomic_id": "extrude_cut_blind",
"profile_type": "obround",
"parameter_roles": ["length_mm", "width_mm", "depth_mm"],
"selector_roles": [],
},
},
],
"feature_order": ["base_plate", "mounting_holes", "adjustment_slot"],
"assumptions": [],
"open_questions": [],
"capability_gaps": [],
"verification_expectations": [
{"type": "feature_count", "feature_id": "mounting_holes", "expected": 4},
{"type": "symmetry", "axis": "x"},
{"type": "bbox", "expected_mm": {"x": 100, "y": 60, "z": 10}},
],
"status": "ready",
}
def mounting_plate_cdsl() -> dict:
return {
"schema": "cad.cdsl.llm.v1",
"schema_version": "1.0",
"kind": "part",
"part_id": "mounting-plate",
"geometry": {
"sketches": [
{"id": "base_sketch", "workplane": workplane(), "profile": {"type": "rectangle", "center": [0, 0], "width_mm": 100, "height_mm": 60}},
{"id": "holes_sketch", "workplane": workplane(10), "profile": {"type": "circle_grid", "radius_mm": 3, "count_x": 2, "count_y": 2, "spacing_x_mm": 80, "spacing_y_mm": 40, "center_mm": [0, 0]}},
{"id": "slot_sketch", "workplane": workplane(10), "profile": {"type": "obround", "center": [0, 0], "length_mm": 32, "width_mm": 10}},
],
},
"features": [
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "base_sketch", "params": {"distance_mm": 10}},
{"id": "hole_cut", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"], "sketch_id": "holes_sketch", "params": {"distance_mm": 10, "reverse": True}},
{"id": "slot_cut", "atomic_id": "extrude_cut_blind", "depends_on": ["hole_cut"], "sketch_id": "slot_sketch", "params": {"distance_mm": 10, "reverse": True}},
],
}
class DesignIntentValidationTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
sys.path.insert(0, str(ROOT / "backend" / "engine"))
import cdsl_engine
cls.engine = cdsl_engine
def assert_plan_error(self, intent: dict, code: str = "INVALID_DESIGN_INTENT") -> None:
with self.assertRaises(self.engine.DesignIntentError) as context:
self.engine.validate_design_intent(intent, self.engine)
self.assertEqual(context.exception.code, code)
def test_valid_mounting_plate_plan_and_cdsl_match(self) -> None:
intent = mounting_plate_intent()
validated = self.engine.validate_design_intent(intent, self.engine)
self.assertEqual(validated["feature_order"], ["base_plate", "mounting_holes", "adjustment_slot"])
self.engine.validate_intent_cdsl(validated, mounting_plate_cdsl(), self.engine)
def test_revise_requires_current_base_revision(self) -> None:
intent = mounting_plate_intent()
intent["mode"] = "revise"
intent["base_revision_id"] = ""
self.assert_plan_error(intent)
intent["base_revision_id"] = "rev_001"
self.engine.validate_design_intent(intent, self.engine, current_revision_id="rev_001")
with self.assertRaises(self.engine.DesignIntentError) as context:
self.engine.validate_design_intent(intent, self.engine, current_revision_id="rev_002")
self.assertEqual(context.exception.code, "DESIGN_INTENT_BASE_REVISION_MISMATCH")
self.assertIn("rev_002", str(context.exception))
def test_rejects_duplicate_ids_bad_order_and_unregistered_capabilities(self) -> None:
duplicate = mounting_plate_intent()
duplicate["structures"][1]["id"] = "base_plate"
self.assert_plan_error(duplicate)
cyclic = mounting_plate_intent()
cyclic["structures"][0]["depends_on"] = ["mounting_holes"]
self.assert_plan_error(cyclic)
unsupported = mounting_plate_intent()
unsupported["structures"][0]["cdsl_strategy"]["atomic_id"] = "thread_add"
self.assert_plan_error(unsupported)
incomplete = mounting_plate_intent()
incomplete["feature_order"].pop()
self.assert_plan_error(incomplete)
no_profile = mounting_plate_intent()
no_profile["structures"][0]["cdsl_strategy"].pop("profile_type")
self.assert_plan_error(no_profile)
unknown_expectation = mounting_plate_intent()
unknown_expectation["verification_expectations"][0]["feature_id"] = "missing"
self.assert_plan_error(unknown_expectation)
def test_blockers_and_nonblocking_approximations_are_explicit(self) -> None:
clarification = mounting_plate_intent()
clarification["open_questions"] = [{"id": "thickness", "question": "What thickness is required?", "blocking": True}]
clarification["status"] = "needs_clarification"
self.engine.validate_design_intent(clarification, self.engine)
with self.assertRaises(self.engine.DesignIntentError) as context:
self.engine.validate_intent_cdsl(clarification, mounting_plate_cdsl(), self.engine)
self.assertEqual(context.exception.code, "DESIGN_INTENT_BLOCKED")
approximation = mounting_plate_intent()
approximation["capability_gaps"] = [{
"code": "unsupported_thread_geometry",
"structure_id": "mounting_holes",
"message": "Only a cylindrical bore is available.",
"blocking": False,
}]
self.assert_plan_error(approximation)
approximation["assumptions"] = ["Thread geometry is approximated by cylindrical bores."]
self.engine.validate_design_intent(approximation, self.engine)
def test_rejects_cdsl_feature_atomic_profile_and_selector_mismatches(self) -> None:
missing_feature = mounting_plate_cdsl()
missing_feature["features"].pop()
with self.assertRaises(self.engine.DesignIntentError) as context:
self.engine.validate_intent_cdsl(mounting_plate_intent(), missing_feature, self.engine)
self.assertEqual(context.exception.code, "INTENT_CDSL_MISMATCH")
atomic = mounting_plate_cdsl()
atomic["features"][1]["atomic_id"] = "extrude_add_blind"
with self.assertRaises(self.engine.DesignIntentError):
self.engine.validate_intent_cdsl(mounting_plate_intent(), atomic, self.engine)
profile = mounting_plate_cdsl()
profile["geometry"]["sketches"][1]["profile"]["type"] = "circles"
with self.assertRaises(self.engine.DesignIntentError):
self.engine.validate_intent_cdsl(mounting_plate_intent(), profile, self.engine)
selector_plan = copy.deepcopy(mounting_plate_intent())
selector_plan["structures"][2]["cdsl_strategy"]["selector_roles"] = ["host_face"]
with self.assertRaises(self.engine.DesignIntentError):
self.engine.validate_intent_cdsl(selector_plan, mounting_plate_cdsl(), self.engine)
+37 -57
View File
@@ -14,20 +14,42 @@ 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 _workplane(z: float = 0.0) -> dict[str, list[float]]:
return {"origin_mm": [0.0, 0.0, z], "x_dir": [1.0, 0.0, 0.0], "y_dir": [0.0, 1.0, 0.0], "normal": [0.0, 0.0, 1.0]}
def mounting_plate_cdsl() -> dict:
holes = [
{"role": "outer", "closed": True, "segments": [{"type": "circle", "center": center, "radius_mm": 3}]}
for center in [[-40, -20], [40, -20], [-40, 20], [40, 20]]
]
return {
"schema": "cad.cdsl.llm.v1",
"schema_version": "1.0",
"kind": "part",
"part_id": "mounting-plate",
"geometry": {"sketches": [
{"id": "base_sketch", "workplane": _workplane(), "profile": {"type": "polygon", "vertices": [[-50, -30], [50, -30], [50, 30], [-50, 30]]}},
{"id": "holes_sketch", "workplane": _workplane(10), "profile": {"type": "analytic_contours", "contours": holes}},
]},
"features": [
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "base_sketch", "params": {"distance_mm": 10}},
{"id": "hole_cut", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"], "sketch_id": "holes_sketch", "params": {"distance_mm": 10, "reverse": True}},
],
}
class DirectCdslFlowTests(unittest.TestCase):
def settings(self, root: Path) -> Settings:
provider = ProviderConfig("test", "Test", "https://example.invalid/v1", "test-key", (ProviderModel("test-model"),))
return Settings(
@@ -48,7 +70,7 @@ class DesignIntentFlowTests(unittest.TestCase):
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": ""}
state = {"phase": "INTAKE", "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))
@@ -77,7 +99,7 @@ class DesignIntentFlowTests(unittest.TestCase):
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."}
state = {"phase": "PLANNED", "design_brief": "Create a flange base."}
captured_build: dict[str, object] = {}
def fake_build_revision(**kwargs: object) -> dict[str, object]:
@@ -99,45 +121,6 @@ class DesignIntentFlowTests(unittest.TestCase):
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:
@@ -148,7 +131,7 @@ class DesignIntentFlowTests(unittest.TestCase):
"role": "assistant", "content": "", "tool_calls": [{
"id": "brief", "type": "function", "function": {
"name": "describe_design_intent",
"arguments": json.dumps({"plan": "Create a flange sleeve.", "assumptions": []}),
"arguments": json.dumps({"plan": "Create a cylindrical part.", "assumptions": []}),
},
}],
}}],
@@ -156,11 +139,11 @@ class DesignIntentFlowTests(unittest.TestCase):
{
"choices": [{"message": {
"role": "assistant", "content": "", "tool_calls": [{
"id": "template", "type": "function", "function": {
"name": "generate_flange_sleeve_model",
"id": "generate", "type": "function", "function": {
"name": "generate_cdsl_model",
"arguments": json.dumps({
"plan": {"template": TEMPLATE_ID},
"summary": "flange sleeve",
"cdsl": mounting_plate_cdsl(),
"summary": "mounting plate",
"assumptions": [],
}),
},
@@ -189,7 +172,7 @@ class DesignIntentFlowTests(unittest.TestCase):
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")])
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
@@ -270,8 +253,7 @@ class DesignIntentFlowTests(unittest.TestCase):
"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):
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 mounting plate")])
async for _ in agent.stream([message], None, None):
@@ -281,18 +263,16 @@ class DesignIntentFlowTests(unittest.TestCase):
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("structures", brief_result)
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": ""}
state = {"phase": "INTAKE", "design_brief": "", "current_model_read": True}
asyncio.run(agent._run_tool(
"describe_design_intent",
{"plan": "Increase the plate thickness and preserve the existing hole layout.", "assumptions": []},
+360
View File
@@ -0,0 +1,360 @@
from __future__ import annotations
import asyncio
import json
import subprocess
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.services.agent_service import AgentService # noqa: E402
from app.services.cdsl_patch import CdslPatchError, apply_cdsl_patch # noqa: E402
from app.services.engine_service import QualityVerificationError, build_revision # noqa: E402
from app.services.library import CdslLibrary # noqa: E402
from app.services.part_skills import PartSkillLibrary # noqa: E402
from app.services.quality import evaluate_quality, validate_verification # noqa: E402
from app.services.storage import WorkspaceStore, write_json # noqa: E402
from app.settings import ProviderConfig, ProviderModel, Settings # noqa: E402
BACKEND = ROOT / "backend"
PART_SKILL_ROOT = BACKEND / "agent" / "skills" / "cad-engine" / "references" / "part-skills"
def workplane(z: float = 0.0) -> dict[str, list[float]]:
return {
"origin_mm": [0.0, 0.0, z],
"x_dir": [1.0, 0.0, 0.0],
"y_dir": [0.0, 1.0, 0.0],
"normal": [0.0, 0.0, 1.0],
}
def fixture() -> dict:
return {
"schema": "cad.cdsl.llm.v1",
"schema_version": "1.0",
"kind": "part",
"part_id": "direct-cdsl-fixture",
"geometry": {
"sketches": [{
"id": "base",
"workplane": workplane(),
"profile": {"type": "polygon", "vertices": [[-10, -5], [10, -5], [10, 5], [-10, 5]]},
}],
},
"features": [{
"id": "base_add",
"atomic_id": "extrude_add_blind",
"depends_on": [],
"sketch_id": "base",
"params": {"distance_mm": 4},
}],
}
def settings(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,),
)
class PatchTests(unittest.TestCase):
def test_rfc6902_patch_updates_cdsl_without_replacing_its_root(self) -> None:
document = fixture()
patched = apply_cdsl_patch(document, [{"op": "replace", "path": "/features/0/params/distance_mm", "value": 8}])
self.assertEqual(patched["features"][0]["params"]["distance_mm"], 8)
self.assertEqual(document["features"][0]["params"]["distance_mm"], 4)
with self.assertRaisesRegex(CdslPatchError, "complete CDSL"):
apply_cdsl_patch(document, [{"op": "replace", "path": "", "value": {}}])
def test_invalid_patch_path_is_rejected(self) -> None:
with self.assertRaisesRegex(CdslPatchError, "out of range"):
apply_cdsl_patch(fixture(), [{"op": "replace", "path": "/features/3/id", "value": "x"}])
class VerificationTests(unittest.TestCase):
def test_bbox_accepts_dimension_vector(self) -> None:
rules = validate_verification({"rules": [
{"id": "size", "type": "bbox", "expected": [20, 10, 4]},
]}, fixture())
report = evaluate_quality(rules, fixture(), {
"bbox_mm": {"min": [-10, -5, 0], "max": [10, 5, 4]},
})
self.assertEqual(report["status"], "passed")
self.assertEqual(report["results"][0]["actual"]["dimensions"], [20.0, 10.0, 4.0])
def test_bbox_accepts_coordinate_ranges(self) -> None:
rules = validate_verification({"rules": [
{"id": "range", "type": "bbox", "expected": {
"x_min": -10, "x_max": 10, "y_min": -5, "y_max": 5, "z_min": 0, "z_max": 4,
}},
]}, fixture())
report = evaluate_quality(rules, fixture(), {
"bbox_mm": {"min": [-10, -5, 0], "max": [10, 5, 4]},
})
self.assertEqual(report["status"], "passed")
def test_feature_bbox_uses_owned_topology_records(self) -> None:
rules = validate_verification({"rules": [
{"id": "base_range", "type": "bbox", "feature": "base_add", "expected": [10, 8, 2]},
]}, fixture())
report = evaluate_quality(rules, fixture(), {
"bbox_mm": {"min": [0, 0, 0], "max": [20, 20, 20]},
"topology_records": [{
"feature_id": "base_add", "owner_feature_ids": ["base_add"],
"geometry": {"bbox_mm": [0, 0, 0, 10, 8, 2]},
}],
})
self.assertEqual(report["status"], "passed")
self.assertEqual(report["results"][0]["source"], "runtime.topology_records[base_add].bbox_mm")
def test_bbox_shape_is_rejected_before_execution(self) -> None:
with self.assertRaisesRegex(ValueError, "expected for bbox"):
validate_verification({"rules": [
{"id": "bad", "type": "bbox", "expected": {"width": 10}},
]}, fixture())
def test_unknown_feature_reference_is_rejected(self) -> None:
with self.assertRaisesRegex(ValueError, "must reference a CDSL feature ID"):
validate_verification({"rules": [{"id": "holes", "type": "hole_count", "feature": "missing", "expected": 4}]}, fixture())
def test_invalid_expected_value_is_rejected(self) -> None:
with self.assertRaisesRegex(ValueError, "expected for bbox"):
validate_verification({"rules": [{"id": "size", "type": "bbox", "expected": [10, 5]}]}, fixture())
def test_warning_does_not_block_generic_quality(self) -> None:
rules = validate_verification({"rules": [
{"id": "solids", "type": "solid_count", "expected": 1, "severity": "blocking"},
{"id": "length", "type": "overall_length", "expected": 999, "severity": "warning"},
]}, fixture())
report = evaluate_quality(rules, fixture(), {
"solid_count": 1,
"bbox_mm": {"min": [-10, -5, 0], "max": [10, 5, 4]},
})
self.assertEqual(report["status"], "passed")
self.assertEqual(len(report["blocking_failures"]), 0)
self.assertEqual(len(report["warnings"]), 1)
def test_blocking_failure_requires_repair(self) -> None:
rules = validate_verification({"rules": [{"id": "solids", "type": "solid_count", "expected": 2}]}, fixture())
report = evaluate_quality(rules, fixture(), {"solid_count": 1, "bbox_mm": {"min": [0, 0, 0], "max": [1, 1, 1]}})
self.assertEqual(report["status"], "failed")
self.assertEqual(report["blocking_failures"][0]["id"], "solids")
class AgentPatchFlowTests(unittest.TestCase):
def _seed_revision(self, store: WorkspaceStore) -> tuple[str, str]:
task = store.ensure_task(None, "Create a direct CDSL fixture")
revision_id = "rev_001"
revision_dir = store.task_dir(task["task_id"]) / "revisions" / revision_id
revision_dir.mkdir(parents=True)
write_json(revision_dir / "model.cdsl.json", fixture())
store.update_task(task["task_id"], {
"revision_id": revision_id,
"status": "success",
"cdsl_path": f"revisions/{revision_id}/model.cdsl.json",
})
return task["task_id"], revision_id
def test_patch_creates_a_child_revision_from_explicit_parent(self) -> None:
with tempfile.TemporaryDirectory() as directory:
config = settings(Path(directory))
store = WorkspaceStore(config)
task_id, parent = self._seed_revision(store)
agent = AgentService(config, store, CdslLibrary(config), PartSkillLibrary(PART_SKILL_ROOT))
captured: dict[str, object] = {}
def fake_build_revision(**kwargs: object) -> dict[str, object]:
captured.update(kwargs)
return {"task_id": task_id, "revision_id": "rev_002"}
with patch("app.services.agent_service.build_revision", side_effect=fake_build_revision):
result, _ = asyncio.run(agent._run_tool(
"patch_cdsl_model",
{
"base_revision_id": parent,
"patches": [{"op": "replace", "path": "/features/0/params/distance_mm", "value": 8}],
"summary": "Increase thickness",
"assumptions": [],
},
task_id,
"Increase thickness",
[],
planning_state={"phase": "PLANNED", "current_model_read": True},
))
self.assertTrue(result["ok"])
self.assertEqual(captured["parent_revision_id"], parent)
self.assertEqual(captured["operation"]["type"], "cdsl_patch")
self.assertEqual(captured["cdsl"]["features"][0]["params"]["distance_mm"], 8)
def test_bad_patch_creates_no_revision(self) -> None:
with tempfile.TemporaryDirectory() as directory:
config = settings(Path(directory))
store = WorkspaceStore(config)
task_id, parent = self._seed_revision(store)
agent = AgentService(config, store, CdslLibrary(config), PartSkillLibrary(PART_SKILL_ROOT))
with self.assertRaisesRegex(ValueError, "INVALID_CDSL_PATCH"):
asyncio.run(agent._run_tool(
"patch_cdsl_model",
{"base_revision_id": parent, "patches": [{"op": "replace", "path": "/missing", "value": 8}], "summary": "bad", "assumptions": []},
task_id,
"bad patch",
[],
planning_state={"phase": "PLANNED"},
))
task = store.read_task(task_id)
self.assertEqual([item["revision_id"] for item in task["revisions"]], [parent])
def test_complete_replacement_uses_current_revision_as_parent(self) -> None:
with tempfile.TemporaryDirectory() as directory:
config = settings(Path(directory))
store = WorkspaceStore(config)
task_id, parent = self._seed_revision(store)
agent = AgentService(config, store, CdslLibrary(config), PartSkillLibrary(PART_SKILL_ROOT))
captured: dict[str, object] = {}
def fake_build_revision(**kwargs: object) -> dict[str, object]:
captured.update(kwargs)
return {"task_id": task_id, "revision_id": "rev_002"}
with patch("app.services.agent_service.build_revision", side_effect=fake_build_revision):
result, _ = asyncio.run(agent._run_tool(
"generate_cdsl_model",
{"cdsl": fixture(), "summary": "Replacement", "assumptions": []},
task_id,
"replace geometry",
[],
planning_state={"phase": "PLANNED", "current_model_read": True},
))
self.assertTrue(result["ok"])
self.assertEqual(captured["parent_revision_id"], parent)
self.assertEqual(captured["operation"]["type"], "cdsl_replacement")
def test_blocking_quality_failure_is_repairable_and_readable(self) -> None:
with tempfile.TemporaryDirectory() as directory:
config = settings(Path(directory))
store = WorkspaceStore(config)
with self.assertRaises(QualityVerificationError) as context:
build_revision(
settings=config,
store=store,
task_id=None,
request="Create fixture",
cdsl=fixture(),
reference_ids=[],
summary="fixture",
verification={"rules": [{"id": "wrong-solid-count", "type": "solid_count", "expected": 2}]},
)
error = context.exception
self.assertTrue(error.task_id)
self.assertEqual(error.revision_id, "rev_001")
task = store.read_task(error.task_id)
self.assertEqual(task["revisions"][0]["status"], "needs_repair")
self.assertEqual(task["revisions"][0]["quality_status"], "needs_repair")
agent = AgentService(config, store, CdslLibrary(config), PartSkillLibrary(PART_SKILL_ROOT))
read, _ = asyncio.run(agent._run_tool(
"read_current_cdsl", {}, error.task_id, "Repair fixture", [], planning_state={"phase": "INTAKE"}
))
self.assertTrue(read["ok"])
self.assertEqual(read["revision_id"], error.revision_id)
def test_runtime_build_failure_does_not_leave_a_revision(self) -> None:
with tempfile.TemporaryDirectory() as directory:
config = settings(Path(directory))
store = WorkspaceStore(config)
class BrokenEngine:
def run_cdsl_only(self, _cdsl: dict, _step: Path) -> dict:
raise RuntimeError("intentional runtime failure")
with patch("app.services.engine_service.load_engine", return_value=BrokenEngine()), patch(
"app.services.engine_service.validate_cdsl", return_value=None
), self.assertRaisesRegex(RuntimeError, "intentional runtime failure"):
build_revision(
settings=config,
store=store,
task_id=None,
request="broken fixture",
cdsl=fixture(),
reference_ids=[],
summary="broken",
)
tasks = list(config.task_root.glob("cad_*"))
self.assertEqual(len(tasks), 1)
task = store.read_task(tasks[0].name)
self.assertEqual(task["revisions"], [])
class CleanupAndBoundaryTests(unittest.TestCase):
def test_cleanup_is_dry_run_then_removes_only_retired_artifacts(self) -> None:
with tempfile.TemporaryDirectory() as directory:
data_root = Path(directory) / "data"
task_dir = data_root / "tasks" / "cad_aaaaaaaaaaaa" / "revisions" / "rev_001"
conversation_dir = data_root / "conversations" / "conv_aaaaaaaaaaaa" / "diagnostics"
task_dir.mkdir(parents=True)
conversation_dir.mkdir(parents=True)
write_json(task_dir / "generation-spec.json", {"schema": "cad.generation-spec.v1"})
write_json(task_dir / "model.cdsl.json", fixture())
(task_dir / "model.step").write_bytes(b"step")
(task_dir / "model.glb").write_bytes(b"glb")
write_json(data_root / "tasks" / "cad_aaaaaaaaaaaa" / "task.json", {
"generation_spec_path": "revisions/rev_001/generation-spec.json",
"current_design_intent_id": "intent_aaaaaaaaaaaa",
"revisions": [{"acceptance_path": "revisions/rev_001/acceptance-report.json"}],
})
write_json(conversation_dir / "generation_spec_validation_deadbeef.json", {"kind": "old"})
script = BACKEND / "scripts" / "remove_generation_spec_artifacts.py"
dry = subprocess.run([sys.executable, str(script), "--data-root", str(data_root)], check=True, capture_output=True, text=True)
self.assertIn("Dry run only", dry.stdout)
self.assertTrue((task_dir / "generation-spec.json").is_file())
subprocess.run([sys.executable, str(script), "--data-root", str(data_root), "--apply"], check=True, capture_output=True, text=True)
self.assertFalse((task_dir / "generation-spec.json").exists())
self.assertFalse((conversation_dir / "generation_spec_validation_deadbeef.json").exists())
self.assertTrue((task_dir / "model.cdsl.json").is_file())
self.assertTrue((task_dir / "model.step").is_file())
self.assertTrue((task_dir / "model.glb").is_file())
cleaned = json.loads((data_root / "tasks" / "cad_aaaaaaaaaaaa" / "task.json").read_text(encoding="utf-8"))
self.assertNotIn("generation_spec_path", cleaned)
self.assertNotIn("current_design_intent_id", cleaned)
self.assertNotIn("acceptance_path", cleaned["revisions"][0])
def test_engine_does_not_import_application_or_dispatch_part_families(self) -> None:
engine_root = BACKEND / "engine" / "cdsl_engine"
source = "\n".join(path.read_text(encoding="utf-8") for path in engine_root.rglob("*.py"))
lowered = source.casefold()
self.assertNotIn("from app", lowered)
self.assertNotIn("import app", lowered)
for business_family in ("mounting_bracket", "mounting_plate", "flange_sleeve", "bearing_housing", "hex_nut"):
self.assertNotIn(business_family, lowered)
@@ -21,6 +21,13 @@ def _workplane() -> dict:
return {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]}
def _rectangle(minimum: list[float], maximum: list[float]) -> dict:
return {"type": "polygon", "vertices": [
[minimum[0], minimum[1]], [maximum[0], minimum[1]],
[maximum[0], maximum[1]], [minimum[0], maximum[1]],
]}
class EngineRuntimeFoundationTests(unittest.TestCase):
def _base_block(self) -> dict:
return {
@@ -28,7 +35,7 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
"meta": {"unit": "mm"},
"geometry": {"sketches": [{
"id": "base", "workplane": _workplane(),
"profile": {"type": "rectangle", "center": [0, 0], "width_mm": 10, "height_mm": 10},
"profile": _rectangle([-5, -5], [5, 5]),
}]},
"features": [{
"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [],
@@ -490,7 +497,7 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "mirror-test",
"meta": {"unit": "mm"},
"geometry": {"sketches": [
{"id": "base", "workplane": _workplane(), "profile": {"type": "rectangle", "center": [0, 0], "width_mm": 10, "height_mm": 10}},
{"id": "base", "workplane": _workplane(), "profile": _rectangle([-5, -5], [5, 5])},
{"id": "cut", "workplane": _workplane(), "profile": {"type": "circle", "center": [2, 0], "radius_mm": 1}},
]},
"features": [
@@ -518,7 +525,7 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
"schema": "cad.cdsl.llm.v1",
"geometry": {"sketches": [{
"id": "circles", "workplane": _workplane(),
"profile": {"type": "circles", "items": [{"center": [2, 3], "radius_mm": 1}]},
"profile": {"type": "circle", "center": [2, 3], "radius_mm": 1},
}]},
"features": [],
}
@@ -761,7 +768,7 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
"meta": {"unit": "mm"},
"geometry": {"sketches": [{
"id": "profile", "workplane": _workplane(),
"profile": {"type": "rectangle", "min_mm": [2, 1], "max_mm": [4, 2]},
"profile": _rectangle([2, 1], [4, 2]),
}]},
"features": [
{
+60 -99
View File
@@ -1,7 +1,6 @@
from __future__ import annotations
import asyncio
import copy
import json
import sys
import tempfile
@@ -14,7 +13,6 @@ sys.path.insert(0, str(ROOT / "backend"))
from app.models.contracts import ChatMessage, MessagePart # noqa: E402
from app.services.agent_service import AgentService, TOOL_SCHEMAS, system_prompt # noqa: E402
from app.services.engine_service import build_revision, load_engine, validate_cdsl # 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
@@ -46,10 +44,49 @@ def document(part_id: str, sketches: list[dict], features: list[dict]) -> dict:
}
def rectangle(center: list[float], width: float, height: float) -> dict:
half_width, half_height = width / 2, height / 2
return {"type": "polygon", "vertices": [
[center[0] - half_width, center[1] - half_height], [center[0] + half_width, center[1] - half_height],
[center[0] + half_width, center[1] + half_height], [center[0] - half_width, center[1] + half_height],
]}
def circles(items: list[dict]) -> dict:
return {"type": "analytic_contours", "contours": [
{"role": "outer", "closed": True, "segments": [{"type": "circle", "center": item["center"], "radius_mm": item["radius_mm"]}]}
for item in items
]}
def circle_grid(radius: float, count_x: int, count_y: int, spacing_x: float, spacing_y: float) -> dict:
return circles([
{"center": [(column - (count_x - 1) / 2) * spacing_x, (row - (count_y - 1) / 2) * spacing_y], "radius_mm": radius}
for row in range(count_y) for column in range(count_x)
])
def obround(length: float, width: float) -> dict:
radius, left, right = width / 2, -length / 2 + width / 2, length / 2 - width / 2
return {"type": "analytic_contours", "contours": [{"role": "outer", "closed": True, "segments": [
{"type": "line", "start": [right, radius], "end": [left, radius]},
{"type": "arc", "start": [left, radius], "end": [left, -radius], "center": [left, 0], "radius_mm": radius},
{"type": "line", "start": [left, -radius], "end": [right, -radius]},
{"type": "arc", "start": [right, -radius], "end": [right, radius], "center": [right, 0], "radius_mm": radius},
]}]}
def annulus(inner_radius: float, outer_radius: float) -> dict:
return circles([
{"center": [0, 0], "radius_mm": outer_radius},
{"center": [0, 0], "radius_mm": inner_radius},
])
def mounting_plate_fixture() -> dict:
return document("golden-mounting-plate", [
{"id": "base", "workplane": workplane(), "profile": {"type": "rectangle", "center": [0, 0], "width_mm": 80, "height_mm": 60}},
{"id": "grid", "workplane": workplane(10), "profile": {"type": "circle_grid", "radius_mm": 3, "count_x": 2, "count_y": 2, "spacing_x_mm": 50, "spacing_y_mm": 30, "center_mm": [0, 0]}},
{"id": "base", "workplane": workplane(), "profile": rectangle([0, 0], 80, 60)},
{"id": "grid", "workplane": workplane(10), "profile": circle_grid(3, 2, 2, 50, 30)},
], [
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [], "params": {"distance_mm": 10}, "sketch_id": "base"},
{"id": "mount_pattern", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"], "params": {"distance_mm": 10, "reverse": True}, "sketch_id": "grid"},
@@ -60,10 +97,10 @@ def mounting_plate_fixture() -> dict:
def mounting_bracket_fixture() -> dict:
web_plane = {"origin_mm": [0, -20, 0], "x_dir": [1, 0, 0], "y_dir": [0, 0, -1], "normal": [0, 1, 0]}
return document("golden-mounting-bracket", [
{"id": "base", "workplane": workplane(), "profile": {"type": "rectangle", "center": [0, 0], "width_mm": 80, "height_mm": 40}},
{"id": "web", "workplane": web_plane, "profile": {"type": "rectangle", "center": [0, -15], "width_mm": 50, "height_mm": 30}},
{"id": "slot", "workplane": workplane(6), "profile": {"type": "obround", "center": [0, 0], "length_mm": 24, "width_mm": 8}},
{"id": "symmetric_holes", "workplane": workplane(6), "profile": {"type": "circles", "items": [{"center": [-25, 0], "radius_mm": 3}, {"center": [25, 0], "radius_mm": 3}]}},
{"id": "base", "workplane": workplane(), "profile": rectangle([0, 0], 80, 40)},
{"id": "web", "workplane": web_plane, "profile": rectangle([0, -15], 50, 30)},
{"id": "slot", "workplane": workplane(6), "profile": obround(24, 8)},
{"id": "symmetric_holes", "workplane": workplane(6), "profile": circles([{"center": [-25, 0], "radius_mm": 3}, {"center": [25, 0], "radius_mm": 3}])},
], [
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [], "params": {"distance_mm": 6}, "sketch_id": "base"},
{"id": "web_add", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"], "params": {"distance_mm": 6}, "sketch_id": "web"},
@@ -74,8 +111,8 @@ def mounting_bracket_fixture() -> dict:
def flange_fixture() -> dict:
return document("golden-flange", [
{"id": "base", "workplane": workplane(), "profile": {"type": "annulus", "inner_radius_mm": 10, "outer_radius_mm": 40}},
{"id": "bolt_circle", "workplane": workplane(12), "profile": {"type": "circles", "items": [{"center": [25, 0], "radius_mm": 3}, {"center": [0, 25], "radius_mm": 3}, {"center": [-25, 0], "radius_mm": 3}, {"center": [0, -25], "radius_mm": 3}]}},
{"id": "base", "workplane": workplane(), "profile": annulus(10, 40)},
{"id": "bolt_circle", "workplane": workplane(12), "profile": circles([{"center": [25, 0], "radius_mm": 3}, {"center": [0, 25], "radius_mm": 3}, {"center": [-25, 0], "radius_mm": 3}, {"center": [0, -25], "radius_mm": 3}])},
], [
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [], "params": {"distance_mm": 12}, "sketch_id": "base"},
{"id": "bolt_holes", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"], "params": {"distance_mm": 12, "reverse": True}, "sketch_id": "bolt_circle"},
@@ -94,9 +131,9 @@ def shaft_fixture() -> dict:
def bearing_housing_fixture() -> dict:
return document("golden-bearing-housing", [
{"id": "base", "workplane": workplane(), "profile": {"type": "rectangle", "center": [0, 0], "width_mm": 100, "height_mm": 60}},
{"id": "base", "workplane": workplane(), "profile": rectangle([0, 0], 100, 60)},
{"id": "housing", "workplane": workplane(8), "profile": {"type": "circle", "radius_mm": 28}},
{"id": "base_holes", "workplane": workplane(8), "profile": {"type": "circle_grid", "radius_mm": 4, "count_x": 2, "count_y": 2, "spacing_x_mm": 80, "spacing_y_mm": 40, "center_mm": [0, 0]}},
{"id": "base_holes", "workplane": workplane(8), "profile": circle_grid(4, 2, 2, 80, 40)},
], [
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [], "params": {"distance_mm": 8}, "sketch_id": "base"},
{"id": "housing_add", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"], "params": {"distance_mm": 25}, "sketch_id": "housing"},
@@ -115,16 +152,6 @@ def hex_nut_fixture() -> dict:
])
GOLDEN_CASES = (
("带沉孔和孔阵列的安装底板", mounting_plate_fixture, {"planning/mounting-plate", "atomic/counterbored-hole-creation", "atomic/pattern-holes-from-datum"}, {"atomic/pattern-holes-from-datum": "expanded"}),
("带槽和对称孔的安装支架", mounting_bracket_fixture, {"planning/mounting-bracket", "functional/slotted-adjustment-feature", "functional/symmetric-feature-layout"}, {"atomic/pattern-holes-from-datum": "expanded"}),
("带螺栓圆的法兰", flange_fixture, {"planning/flange", "functional/flange-bolt-circle"}, {"functional/flange-bolt-circle": "expanded"}),
("带回转体同轴孔的阶梯轴", shaft_fixture, {"planning/simple-shaft", "functional/axisymmetric-revolve-strategy", "atomic/coaxial-bore-rule"}, {}),
("带轴承座孔和底座孔的轴承座", bearing_housing_fixture, {"planning/bearing-housing", "functional/bearing-bore-seat"}, {}),
("M6 六角螺母", hex_nut_fixture, {"planning/hexagonal-nut", "atomic/threaded-hole-creation"}, {"atomic/threaded-hole-creation": "approximated"}),
)
class PartSkillLibraryTests(unittest.TestCase):
def setUp(self) -> None:
self.library = PartSkillLibrary(PART_SKILL_ROOT)
@@ -166,24 +193,19 @@ class PartSkillLibraryTests(unittest.TestCase):
self.assertTrue((PART_SKILL_ROOT / skill["bridge"]).is_file())
self.assertTrue((PART_SKILL_ROOT / skill["source"]).is_file())
self.assertTrue(skill["triggers"])
self.assertTrue(skill["capability_translation_rules"])
self.assertNotIn("capability_translation_rules", skill)
self.assertEqual(categories, {"planning": 6, "functional": 7, "atomic": 8})
self.assertTrue((PART_SKILL_ROOT / "LICENSE").is_file())
self.assertTrue((PART_SKILL_ROOT / "PROVENANCE.md").is_file())
def test_audit_records_exact_omitted_and_blocked_capability_states(self) -> None:
def test_audit_records_selection_and_assumptions_without_interpreting_geometry(self) -> None:
fixture = mounting_plate_fixture()
exact = self.library.audit(self.library.select("安装底板"), fixture)
finishing = self.library.audit(self.library.select("edge treatment"), fixture)
blocked = self.library.audit(self.library.select("flange bolt circle"), mounting_plate_fixture())
exact_statuses = {item["skill_id"]: item["status"] for item in exact["capability_translations"]}
finishing_statuses = {item["skill_id"]: item for item in finishing["capability_translations"]}
blocked_statuses = {item["skill_id"]: item["status"] for item in blocked["capability_translations"]}
self.assertEqual(exact_statuses["planning/mounting-plate"], "exact")
self.assertEqual(finishing_statuses["atomic/fillet-chamfer-last"]["status"], "omitted")
self.assertEqual(finishing_statuses["atomic/fillet-chamfer-last"]["translation"], "selector_unavailable")
self.assertEqual(blocked_statuses["functional/flange-bolt-circle"], "blocked")
audit = self.library.audit(self.library.select("安装底板"), fixture, ["孔位置由用户确认"])
self.assertIn("planning/mounting-plate", audit["skill_ids"])
self.assertEqual(audit["assumptions"], ["孔位置由用户确认"])
self.assertTrue(all(item.get("version") and item.get("summary") for item in audit["skills"]))
self.assertNotIn("capability_translations", audit)
self.assertNotIn("evidence", audit)
class AgentPartSkillTests(unittest.TestCase):
@@ -211,7 +233,7 @@ class AgentPartSkillTests(unittest.TestCase):
self.assertIn("[planning/flange]", str(agent.first_messages[0]["content"]))
self.assertIn("[functional/flange-bolt-circle]", str(agent.first_messages[0]["content"]))
def test_prompt_contains_bridge_and_template_tool(self) -> None:
def test_prompt_contains_bridge_and_direct_cdsl_tools(self) -> None:
library = PartSkillLibrary(PART_SKILL_ROOT)
selection = library.select("Create a flange with a bolt circle")
with tempfile.TemporaryDirectory() as directory:
@@ -220,7 +242,7 @@ class AgentPartSkillTests(unittest.TestCase):
self.assertIn("[planning/flange]", prompt)
self.assertIn("circular-pattern atomic", prompt)
self.assertEqual([tool["function"]["name"] for tool in TOOL_SCHEMAS], ["analyze_image_reference", "search_cdsl_library", "read_cdsl_reference", "describe_design_intent", "read_current_cdsl", "generate_flange_sleeve_model", "generate_cdsl_model"])
self.assertEqual([tool["function"]["name"] for tool in TOOL_SCHEMAS], ["analyze_image_reference", "search_cdsl_library", "read_cdsl_reference", "describe_design_intent", "read_current_cdsl", "generate_cdsl_model", "patch_cdsl_model"])
def test_generation_persists_assumptions_and_selected_skills(self) -> None:
with tempfile.TemporaryDirectory() as directory:
@@ -230,7 +252,7 @@ class AgentPartSkillTests(unittest.TestCase):
agent = AgentService(settings, store, CdslLibrary(settings), library)
request = "M6 六角螺母"
selection = library.select(request)
planning_state = {"phase": "WAITING_FOR_PLAN", "design_brief": ""}
planning_state = {"phase": "INTAKE", "design_brief": ""}
planned, _ = asyncio.run(agent._run_tool(
"describe_design_intent",
{"plan": "Create an M6 hex nut with a centered cylindrical bore representing the thread.", "assumptions": []},
@@ -278,66 +300,5 @@ class AgentPartSkillTests(unittest.TestCase):
)
class PartSkillBuildAndGoldenTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.settings = AgentPartSkillTests._settings(Path(tempfile.mkdtemp()))
cls.engine = load_engine(cls.settings)
cls.library = PartSkillLibrary(PART_SKILL_ROOT)
def test_success_and_failure_revisions_always_write_part_skill_audit(self) -> None:
with tempfile.TemporaryDirectory() as directory:
settings = AgentPartSkillTests._settings(Path(directory))
store = WorkspaceStore(settings)
selection = self.library.select("带螺栓圆的法兰")
audit = self.library.audit(selection, flange_fixture(), ["Explicit circle expansion"])
success = build_revision(
settings=settings, store=store, task_id=None, request="带螺栓圆的法兰", cdsl=flange_fixture(),
reference_ids=["flange-reference"], summary="flange", part_skills=audit,
generation_assumptions=["Explicit circle expansion"],
)
success_audit = json.loads(store.artifact_path(success["task_id"], success["part_skills_path"]).read_text(encoding="utf-8"))
success_report = json.loads(store.artifact_path(success["task_id"], success["report_path"]).read_text(encoding="utf-8"))
self.assertEqual(success_audit["skill_ids"], audit["skill_ids"])
self.assertIn("generation_context", success_report)
invalid = copy.deepcopy(flange_fixture())
invalid["features"][0]["atomic_id"] = "not_supported"
failed_audit = self.library.audit(selection, invalid, [])
with self.assertRaisesRegex(ValueError, "CDSL schema violation"):
build_revision(
settings=settings, store=store, task_id=success["task_id"], request="bad flange", cdsl=invalid,
reference_ids=[], summary="bad", part_skills=failed_audit, generation_assumptions=[],
)
task = store.read_task(success["task_id"])
failed = task["revisions"][-1]
self.assertEqual(failed["status"], "failed")
self.assertTrue(store.artifact_path(task["task_id"], failed["part_skills_path"]).is_file())
report = json.loads(store.artifact_path(task["task_id"], failed["report_path"]).read_text(encoding="utf-8"))
self.assertEqual(report["generation_context"]["part_skill_ids"], audit["skill_ids"])
legacy_report = {"engine_result": {"engine": "cdsl_only"}}
self.assertEqual(legacy_report.get("generation_context", {}), {})
def test_golden_part_families_validate_preflight_rebuild_and_audit(self) -> None:
for request, factory, expected_skills, expected_statuses in GOLDEN_CASES:
with self.subTest(request=request):
cdsl = factory()
selection = self.library.select(request)
self.assertTrue(expected_skills.issubset(selection["skill_ids"]))
validate_cdsl(cdsl, self.engine)
with tempfile.TemporaryDirectory() as directory:
step_path = Path(directory) / "model.step"
result = self.engine.run_cdsl_only(copy.deepcopy(cdsl), step_path)
self.assertEqual(result["engine"], "cdsl_only")
self.assertTrue(step_path.is_file())
self.assertGreater(step_path.stat().st_size, 0)
audit = self.library.audit(selection, cdsl, ["golden fixture"])
statuses = {item["skill_id"]: item["status"] for item in audit["capability_translations"]}
for skill_id, status in expected_statuses.items():
self.assertEqual(statuses[skill_id], status)
self.assertTrue(audit["evidence"]["features"])
self.assertTrue(audit["evidence"]["profiles"])
if __name__ == "__main__":
unittest.main()
+28 -40
View File
@@ -6,7 +6,6 @@ import unittest
from pathlib import Path
from app.services.engine_service import load_engine, normalize_cdsl_for_engine, validate_cdsl
from app.services.flange_sleeve_template import TEMPLATE_ID, build_flange_sleeve_cdsl
from app.settings import get_settings
@@ -23,21 +22,38 @@ class ProfileSchemaTests(unittest.TestCase):
def test_schema_and_registered_profiles_stay_in_sync(self) -> None:
self.assertEqual(set(self.schema["runtime_supported_profiles"]), set(self.engine.SHAPE_GENERATORS))
def test_legacy_profile_macros_require_explicit_adapter_lowering(self) -> None:
legacy = {
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "legacy-rectangle",
"meta": {"unit": "mm"},
"geometry": {"sketches": [{
"id": "base", "workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
"profile": {"type": "rectangle", "center": [0, 0], "width_mm": 10, "height_mm": 10},
}]},
"features": [{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "base", "params": {"distance_mm": 2}}],
}
with self.assertRaisesRegex(ValueError, "CDSL schema violation"):
validate_cdsl(legacy, self.engine)
from cdsl_importer.legacy_profile_adapter import lower_legacy_profiles
lowered = lower_legacy_profiles(legacy)
self.assertEqual(lowered["geometry"]["sketches"][0]["profile"]["type"], "analytic_contours")
validate_cdsl(lowered, self.engine)
def test_schema_and_executable_atomic_operations_stay_in_sync(self) -> None:
self.assertEqual(set(self.schema["runtime_supported_atomic_ids"]), set(self.engine.SUPPORTED_ATOMIC_IDS))
self.assertTrue(set(self.engine.SUPPORTED_ATOMIC_IDS).issubset(self.schema["feature_atomic_ids"]))
self.assertEqual(set(self.engine.SUPPORTED_ATOMIC_IDS), set(self.schema["feature_atomic_ids"]))
self.assertEqual(
set(self.cdsl_schema["$defs"]["feature_atomic_ids"]["enum"]),
set(self.schema["feature_atomic_ids"]),
set(self.engine.SUPPORTED_ATOMIC_IDS),
)
def test_machine_schema_and_human_contract_stay_in_sync(self) -> None:
self.assertEqual(
set(self.cdsl_schema["$defs"]["profile_type"]["enum"]),
set(self.schema["profiles"]) - {"complex_arc_shape", "unknown_shape"},
set(self.schema["runtime_supported_profiles"]),
)
self.assertEqual(set(self.cdsl_schema["$defs"]["motif_type"]["enum"]), set(self.schema["motif_types"]))
self.assertEqual(set(self.cdsl_schema["$defs"]["layout_type"]["enum"]), set(self.schema["layout_types"]))
self.assertEqual(set(self.schema["profiles"]), set(self.schema["runtime_supported_profiles"]))
def test_rejects_unsupported_atomic_operation_before_rebuild(self) -> None:
cdsl = {
@@ -135,39 +151,6 @@ class ProfileSchemaTests(unittest.TestCase):
self.assertNotIn("point_mm", axis)
self.assertIn("features[0].params.axis: point_mm -> origin_mm", repairs)
def test_flange_sleeve_template_is_schema_valid_and_runtime_buildable(self) -> None:
cdsl, plan = build_flange_sleeve_cdsl({
"template": TEMPLATE_ID,
"name": "Template flange sleeve",
"flange_width_mm": 120,
"flange_height_mm": 120,
"flange_thickness_mm": 14,
"corner_chamfer_mm": 10,
"tube_outer_diameter_mm": 70,
"tube_straight_length_mm": 95,
"tip_outer_diameter_mm": 62,
"tip_length_mm": 28,
"bore_diameter_mm": 46,
"boss_outer_diameter_mm": 82,
"boss_height_mm": 5,
"mount_hole_diameter_mm": 12,
"mount_counterbore_diameter_mm": 24,
"mount_counterbore_depth_mm": 5,
"mount_hole_u_mm": 42,
"mount_hole_v_mm": 42,
})
self.assertEqual(plan["name"], "Template flange sleeve")
self.assertEqual(cdsl["features"][1]["depends_on"], ["flange_plate"])
self.assertEqual(cdsl["geometry"]["sketches"][0]["workplane"]["x_dir"], [0.0, 1.0, 0.0])
validate_cdsl(cdsl, self.engine)
with tempfile.TemporaryDirectory() as directory:
step_path = Path(directory) / "flange-sleeve.step"
result = self.engine.run_cdsl_only(cdsl, step_path)
self.assertEqual(result["engine"], "cdsl_only")
self.assertTrue(step_path.is_file())
self.assertGreater(step_path.stat().st_size, 0)
def test_cdsl_only_rebuild_preserves_its_actual_failure(self) -> None:
cdsl = {
"schema": "cad.cdsl.llm.v1",
@@ -192,11 +175,16 @@ class ProfileSchemaTests(unittest.TestCase):
self.assertNotIn("engine.run_rebuild(cdsl_copy, step_path)", build_revision_source)
def test_all_official_samples_match_the_engine_schema(self) -> None:
from cdsl_importer.legacy_profile_adapter import lower_legacy_profiles
samples = sorted(self.settings.library_root.glob("samples/**/model.cdsl.json"))
self.assertGreater(len(samples), 0)
for sample_path in samples:
with self.subTest(sample=sample_path.parent.name):
validate_cdsl(json.loads(sample_path.read_text(encoding="utf-8")), self.engine)
legacy_sample = json.loads(sample_path.read_text(encoding="utf-8"))
lowered = lower_legacy_profiles(legacy_sample)
validate_cdsl(lowered, self.engine)
self.assertTrue(self.engine.analyze_cdsl(lowered).runtime_eligible)
if __name__ == "__main__":
unittest.main()