From 91a443d990ea82d597a4de6ee18884fde9df9ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BA=B7?= Date: Fri, 21 Aug 2026 15:22:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + backend/.env | 31 ++++++++++ backend/engine/cdsl_engine/cdsl_schema.json | 12 +++- backend/engine/cdsl_engine/llm_compiler.py | 1 + backend/engine/cdsl_engine/llm_engine.py | 13 +++- .../engine/cdsl_engine/profile_schema.json | 3 +- backend/tests/test_sphere_add.py | 59 +++++++++++++++++++ 7 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 backend/.env create mode 100644 backend/tests/test_sphere_add.py diff --git a/.gitignore b/.gitignore index c602c912..206edf18 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # Environment files .env .env.* +!backend/.env !.env.example !.env.*.example diff --git a/backend/.env b/backend/.env new file mode 100644 index 00000000..eda918ba --- /dev/null +++ b/backend/.env @@ -0,0 +1,31 @@ +# Default provider. Only providers with an API key are exposed to the UI. +CDSL_DEFAULT_PROVIDER=deepseek +CDSL_DEFAULT_MODEL=deepseek-chat + +# DeepSeek. Fill in your own API key below. +CDSL_LLM_BASE_URL=https://api.deepseek.com/v1 +CDSL_LLM_API_KEY=sk-d3f8fe84bf9a4100a6563559e5d2fefd +CDSL_LLM_MODEL=deepseek-chat +CDSL_LLM_TIMEOUT_S=90 + +# Optional OpenAI provider. Comma-separate enabled models; list vision models +# separately so image attachments can be routed safely. +CDSL_OPENAI_BASE_URL=https://api.openai.com/v1 +CDSL_OPENAI_API_KEY= +CDSL_OPENAI_MODELS=gpt-4.1,gpt-4.1-mini +CDSL_OPENAI_VISION_MODELS=gpt-4.1,gpt-4.1-mini + +# Optional Kimi provider. +CDSL_KIMI_BASE_URL=https://api.moonshot.cn/v1 +CDSL_KIMI_API_KEY= +CDSL_KIMI_MODELS=moonshot-v1-8k +CDSL_KIMI_VISION_MODELS= +# Enable only after verifying that the selected endpoint supports strict +# OpenAI-compatible function schemas. This affects generate_cdsl_model's JSON +# arguments only, never ordinary assistant chat text. +CDSL_DEEPSEEK_STRICT_TOOL_SCHEMA=false +CDSL_OPENAI_STRICT_TOOL_SCHEMA=true +CDSL_KIMI_STRICT_TOOL_SCHEMA=true +# To enable individual models instead of every model from a provider, keep the +# provider-wide switch false and list exact IDs, for example: +# CDSL_OPENAI_STRICT_TOOL_MODELS=gpt-4.1 diff --git a/backend/engine/cdsl_engine/cdsl_schema.json b/backend/engine/cdsl_engine/cdsl_schema.json index b6facd17..a8e05d61 100644 --- a/backend/engine/cdsl_engine/cdsl_schema.json +++ b/backend/engine/cdsl_engine/cdsl_schema.json @@ -103,6 +103,15 @@ "required": ["angle_deg", "axis"], "additionalProperties": false }, + "sphereParams": { + "type": "object", + "properties": { + "radius_mm": {"$ref": "#/$defs/positive"}, + "center_mm": {"$ref": "#/$defs/point3"} + }, + "required": ["radius_mm", "center_mm"], + "additionalProperties": false + }, "holeBaseParams": { "type": "object", "properties": { @@ -290,7 +299,7 @@ "required": ["type", "contours"], "additionalProperties": false }, - "feature_atomic_ids": {"enum": ["extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "revolve_add", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore", "fillet", "chamfer", "pattern_linear", "pattern_mirror", "reference_plane", "reference_axis", "hole_wizard"]}, + "feature_atomic_ids": {"enum": ["extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "revolve_add", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore", "sphere_add", "fillet", "chamfer", "pattern_linear", "pattern_mirror", "reference_plane", "reference_axis", "hole_wizard"]}, "feature": { "type": "object", "properties": { @@ -312,6 +321,7 @@ {"if": {"properties": {"atomic_id": {"const": "extrude_cut_blind"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/extrudeParams"}}}}, {"if": {"properties": {"atomic_id": {"const": "revolve_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/revolveParams"}}}}, {"if": {"properties": {"atomic_id": {"const": "revolve_cut"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/revolveParams"}}}}, + {"if": {"properties": {"atomic_id": {"const": "sphere_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/sphereParams"}}}}, {"if": {"properties": {"atomic_id": {"const": "hole_blind"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/holeBlindParams"}}}}, {"if": {"properties": {"atomic_id": {"const": "hole_countersink"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/holeCountersinkParams"}}}}, {"if": {"properties": {"atomic_id": {"const": "hole_counterbore"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/holeCounterboreParams"}}}}, diff --git a/backend/engine/cdsl_engine/llm_compiler.py b/backend/engine/cdsl_engine/llm_compiler.py index 75d8a7ac..0ec2559b 100644 --- a/backend/engine/cdsl_engine/llm_compiler.py +++ b/backend/engine/cdsl_engine/llm_compiler.py @@ -22,6 +22,7 @@ REQUIRED = { "hole_blind": ["diameter_mm", "depth_mm"], "hole_countersink": ["diameter_mm", "depth_mm"], "hole_counterbore": ["diameter_mm", "depth_mm"], + "sphere_add": ["radius_mm", "center_mm"], } diff --git a/backend/engine/cdsl_engine/llm_engine.py b/backend/engine/cdsl_engine/llm_engine.py index 6a4ea8ed..8cff7da3 100644 --- a/backend/engine/cdsl_engine/llm_engine.py +++ b/backend/engine/cdsl_engine/llm_engine.py @@ -28,6 +28,7 @@ from build123d import ( # noqa: E402 Mode, Plane, Polygon, + Sphere, Vector, Wire, export_step, @@ -49,6 +50,7 @@ SUPPORTED_ATOMIC_IDS = frozenset({ "hole_blind", "hole_countersink", "hole_counterbore", + "sphere_add", }) @@ -265,7 +267,16 @@ def run_engine_plan( sketch = step.get("sketch") sid = step.get("step_id") - if atomic in ("extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind"): + if atomic == "sphere_add": + radius = _f(params.get("radius_mm") or 0) + center = params.get("center_mm") or [0, 0, 0] + if radius <= 0 or len(center) != 3: + raise ValueError(f"{sid}: sphere_add requires a positive radius_mm and center_mm") + with Locations((_f(center[0]), _f(center[1]), _f(center[2]))): + Sphere(radius, mode=Mode.ADD) + log.append(f"{sid}: sphere_add radius={radius}") + + elif atomic in ("extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind"): if sketch is None: raise ValueError(f"{sid}: missing sketch") plane = _plane_from_workplane(sketch.get("workplane") or {}) diff --git a/backend/engine/cdsl_engine/profile_schema.json b/backend/engine/cdsl_engine/profile_schema.json index 355b7485..793b8d31 100644 --- a/backend/engine/cdsl_engine/profile_schema.json +++ b/backend/engine/cdsl_engine/profile_schema.json @@ -4,7 +4,7 @@ "cdsl_json_schema_file": "cdsl_schema.json", "maintenance_rule": "The semantic CDSL contract is a superset of the current runtime. runtime_supported_atomic_ids and runtime_supported_profiles must stay synchronized with sketch_solver.py, llm_compiler.py and llm_engine.py; deferred entries describe future engine work.", "coordinate_convention": "All profile dimensions use millimetres. Two-dimensional points are [u, v] in the sketch workplane.", - "runtime_supported_atomic_ids": ["extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "revolve_add", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore"], + "runtime_supported_atomic_ids": ["extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "revolve_add", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore", "sphere_add"], "runtime_supported_profiles": ["circle", "annulus", "circles", "circle_grid", "rectangle", "rectangle_with_circles", "rectangle_with_fillets", "obround", "polygon", "ibone", "rectangle_with_symmetric_notches", "revolve_chamfer", "revolve_chamfer_slanted", "circle_with_arc_notches", "circular_sector_slot", "circle_with_radial_tabs", "filleted_rect_side_slots", "d_shape", "partial_ring", "partial_ring_with_arc_island", "arc_chain", "radial_slot", "patterned_cutouts", "compound_patterned_cutouts", "complex_arc_shape", "unknown_shape"], "feature_atomic_ids": { "extrude_add_blind": {"summary": "Add the closed profile by one signed extrusion distance.", "required_params": ["distance_mm"], "optional_params": ["reverse"], "requires_sketch": true}, @@ -15,6 +15,7 @@ "hole_blind": {"summary": "Cut one or more blind cylindrical holes in the current body.", "required_params": ["diameter_mm", "depth_mm", "positions"], "optional_params": ["host_face", "drill_angle_rad"], "position_format": "positions is a non-empty array of objects: [{\"mm\":[u_mm,v_mm,w_mm]}]. A bare coordinate array is invalid.", "requires_sketch": true}, "hole_countersink": {"summary": "Cut one or more blind holes with countersink dimensions.", "required_params": ["diameter_mm", "depth_mm", "positions", "countersink_diameter_mm", "countersink_angle_rad"], "optional_params": ["host_face", "drill_angle_rad"], "position_format": "positions is a non-empty array of objects: [{\"mm\":[u_mm,v_mm,w_mm]}]. A bare coordinate array is invalid.", "requires_sketch": true}, "hole_counterbore": {"summary": "Cut one or more blind holes with counterbore dimensions.", "required_params": ["diameter_mm", "depth_mm", "positions", "counterbore_diameter_mm", "counterbore_depth_mm"], "optional_params": ["host_face", "drill_angle_rad"], "position_format": "positions is a non-empty array of objects: [{\"mm\":[u_mm,v_mm,w_mm]}]. A bare coordinate array is invalid.", "requires_sketch": true}, + "sphere_add": {"summary": "Add one spherical solid at an explicit model-space center.", "required_params": ["radius_mm", "center_mm"], "optional_params": [], "requires_sketch": true}, "fillet": {"summary": "Apply a radius to selected edges or faces.", "required_params": ["radius_mm"], "optional_params": ["tangent_propagation"], "requires_sketch": false, "execution_status": "deferred"}, "chamfer": {"summary": "Apply an equal-distance or angle-distance chamfer to selected edges or faces.", "required_params": ["distance_mm"], "optional_params": ["distance_2_mm", "angle_rad"], "requires_sketch": false, "execution_status": "deferred"}, "pattern_linear": {"summary": "Repeat source features along one or two directions.", "required_params": ["source_feature_ids", "direction_1", "spacing_1_mm", "pattern_count_1"], "optional_params": ["direction_2", "spacing_2_mm", "pattern_count_2"], "requires_sketch": false, "execution_status": "deferred"}, diff --git a/backend/tests/test_sphere_add.py b/backend/tests/test_sphere_add.py new file mode 100644 index 00000000..948c22eb --- /dev/null +++ b/backend/tests/test_sphere_add.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import sys +import tempfile +import unittest +from pathlib import Path + +from build123d import import_step + + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "backend" / "engine")) + +import cdsl_engine # noqa: E402 + + +class SphereAddTests(unittest.TestCase): + def test_cdsl_only_rebuilds_a_sphere_at_the_requested_center(self) -> None: + cdsl = { + "schema": "cad.cdsl.llm.v1", + "schema_version": "1.0.0", + "kind": "part", + "part_id": "sphere_test", + "meta": {"unit": "mm"}, + "geometry": { + "sketches": [{ + "id": "sphere_locator", + "workplane": { + "origin_mm": [0, 0, 0], + "x_dir": [1, 0, 0], + "normal": [0, 0, 1], + }, + "profile": {"type": "circle", "radius_mm": 1.0}, + }], + }, + "features": [{ + "id": "sphere", + "atomic_id": "sphere_add", + "depends_on": [], + "name": "Test sphere", + "params": {"radius_mm": 2.5, "center_mm": [3.0, -4.0, 5.0]}, + "sketch_id": "sphere_locator", + }], + } + + with tempfile.TemporaryDirectory() as directory: + out_step = Path(directory) / "sphere.step" + result = cdsl_engine.run_rebuild(cdsl, out_step) + shape = import_step(str(out_step)) + bbox = shape.bounding_box() + + self.assertEqual(result["engine"], "cdsl_only") + self.assertAlmostEqual(shape.volume, 4 / 3 * 3.141592653589793 * 2.5 ** 3, places=5) + self.assertEqual((bbox.min.X, bbox.min.Y, bbox.min.Z), (0.5, -6.5, 2.5)) + self.assertEqual((bbox.max.X, bbox.max.Y, bbox.max.Z), (5.5, -1.5, 7.5)) + + +if __name__ == "__main__": + unittest.main()