优化生成流程
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user