diff --git a/backend/README.md b/backend/README.md index e445d238..d2b5371d 100644 --- a/backend/README.md +++ b/backend/README.md @@ -10,39 +10,63 @@ The backend owns the application API and CAD generation workflow: Expected development entrypoint: `app.main:app`, served by Uvicorn. -## Incremental Generation Configuration +## Reasoning Effort -Incremental generation is enabled by default. It requires a separately -configured vision-capable review model and the Python OpenCascade/Pillow -technical renderer; a run -fails instead of skipping visual review when either is unavailable. +The backend uses the Chat Completions API. Configure a provider's reasoning +budget with `CDSL__REASONING_EFFORT`; for the current OpenAI setup: ```dotenv -# Authoring provider/model must already be configured as usual. -CDSL_INCREMENTAL_GENERATION=1 +CDSL_OPENAI_REASONING_EFFORT=medium +``` +Use `low`, `medium`, or `high` according to the latency/cost versus quality +tradeoff. The setting is sent as Chat Completions' `reasoning_effort` field to +authoring, streaming, and visual-review requests. Leave it empty to use the +provider/model default. The selected OpenAI-compatible endpoint must support +the requested value. + +## Autonomous CDSL Agent Configuration + +The autonomous agent writes one frozen free-form `requirements.md`, then +observes, measures, renders and appends one CDSL feature at a time. Its author +uses normal function calls; no provider strict JSON Schema capability or +complete modelling DAG is required. Candidate fragments are rebuilt in a +staging directory through `cdsl_only` before a checkpoint can be committed. + +Final publication requires a separately configured vision-capable review model +and the Python OpenCascade/Pillow technical renderer. The agent may build and +inspect intermediate checkpoints without image review; a final run fails +closed if its independent review configuration is unavailable. + +```dotenv # Must name one configured provider and one model listed in that provider's # CDSL__VISION_MODELS setting. It is intentionally not inferred # from the authoring model. -CDSL_REVIEW_PROVIDER=openai -CDSL_REVIEW_MODEL=gpt-4.1-mini -CDSL_OPENAI_VISION_MODELS=gpt-4.1-mini +CDSL_REVIEW_PROVIDER=deepseek +CDSL_REVIEW_MODEL=deepseek-v4-flash-vision-exp +CDSL_DEEPSEEK_VISION_MODELS=deepseek-v4-flash-vision-exp # Install Python rendering dependencies. The renderer reads the revision STEP # file and creates canonical images without a browser or GPU driver. pip install -r requirements.txt -# Optional per-node retry budgets. -CDSL_NODE_AUTHORING_ATTEMPTS=2 -CDSL_NODE_REPAIR_ATTEMPTS=2 -CDSL_NODE_REPLAN_ATTEMPTS=1 +# Limits apply to the current checkpoint head, never to total task complexity. +CDSL_AGENT_TOOL_CALLS_PER_CYCLE=12 +CDSL_AGENT_CANDIDATE_ATTEMPTS_PER_HEAD=3 +CDSL_AGENT_CONSECUTIVE_NO_PROGRESS_LIMIT=6 +CDSL_AGENT_MAX_FEATURES_PER_FRAGMENT=6 +CDSL_AGENT_CONTEXT_CHAR_LIMIT=24000 +CDSL_AGENT_RENDER_CACHE=true ``` -Every checkpoint is rebuilt from its fully materialized CDSL through the +The author chooses each coherent 1-6 feature batch. Every rebuilt batch is +rendered and independently reviewed before it can become a checkpoint; only +an accepted reviewer verdict advances the working model. Every checkpoint is rebuilt from its fully materialized CDSL through the `cdsl_only` runtime. Checkpoint GLB files are preview-only; STEP, CDSL, and reports are available only after the task reaches `COMPLETED`. -The generation plan contains semantic node IDs only. The backend derives the -unique CDSL feature and sketch IDs from each node, then writes them during -fragment materialization. This keeps naming and topology ownership stable -without requiring the authoring model to reproduce internal identifiers. +The backend assigns feature and sketch IDs, appends causal dependencies and +expands only opaque current-snapshot selector tokens. It does not compile +geometry templates or correct workplanes, profiles, sizes, directions or +boolean semantics authored by the model. Failed candidates remain auditable +but never become revisions. diff --git a/backend/engine/cdsl_engine/build123d_adapter.py b/backend/engine/cdsl_engine/build123d_adapter.py index 343caaaf..562293b4 100644 --- a/backend/engine/cdsl_engine/build123d_adapter.py +++ b/backend/engine/cdsl_engine/build123d_adapter.py @@ -399,9 +399,14 @@ class Build123dGeometryAdapter: def body_geometry(body: Any) -> dict[str, Any]: # 汇总主体基本几何信息:包围盒与体积。 bbox = body.bounding_box() + # A feature history can contain several body IDs while still ending in + # one connected solid (for example, a base extrusion followed by hole + # cuts). Count the current OCC result, never feature history entries. + solids = list(body.solids()) if hasattr(body, "solids") else [body] return { "bbox_mm": [bbox.min.X, bbox.min.Y, bbox.min.Z, bbox.max.X, bbox.max.Y, bbox.max.Z], "volume_mm3": float(body.volume), + "solid_count": len(solids), } @staticmethod diff --git a/backend/engine/cdsl_engine/profile_schema.json b/backend/engine/cdsl_engine/profile_schema.json index 74a90623..cb49168e 100644 --- a/backend/engine/cdsl_engine/profile_schema.json +++ b/backend/engine/cdsl_engine/profile_schema.json @@ -7,22 +7,22 @@ "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", "reference_plane", "reference_axis", "hole_wizard", "fillet", "chamfer", "pattern_linear", "pattern_mirror"], "runtime_supported_profiles": ["circle", "polygon", "analytic_contours"], "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}, - "extrude_add_two_sided": {"summary": "Add the closed profile with independently captured forward and reverse terminations.", "required_params": ["distance_mm", "reverse_distance_mm"], "optional_params": ["reverse", "end_condition", "reverse_end_condition"], "requires_sketch": true}, + "extrude_add_blind": {"summary": "Add the closed profile by one signed extrusion distance.", "required_params": ["distance_mm"], "optional_params": ["reverse"], "requires_sketch": true, "produces_body": true}, + "extrude_add_two_sided": {"summary": "Add the closed profile with independently captured forward and reverse terminations.", "required_params": ["distance_mm", "reverse_distance_mm"], "optional_params": ["reverse", "end_condition", "reverse_end_condition"], "requires_sketch": true, "produces_body": true}, "extrude_cut_blind": {"summary": "Remove the closed profile by one signed extrusion distance.", "required_params": ["distance_mm"], "optional_params": ["reverse"], "requires_sketch": true}, - "revolve_add": {"summary": "Add the closed profile by revolving it around an axis.", "required_params": ["angle_deg", "axis"], "optional_params": ["reverse"], "requires_sketch": true}, + "revolve_add": {"summary": "Add the closed profile by revolving it around an axis.", "required_params": ["angle_deg", "axis"], "optional_params": ["reverse"], "requires_sketch": true, "produces_body": true}, "revolve_cut": {"summary": "Remove the closed profile by revolving it around an axis.", "required_params": ["angle_deg", "axis"], "optional_params": ["reverse"], "requires_sketch": true}, - "hole_blind": {"summary": "Cut one or more blind cylindrical holes in the current body.", "required_params": ["diameter_mm", "depth_mm", "positions", "host_face"], "optional_params": ["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. host_face must provide a strict selector or an explicit frame.", "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", "host_face"], "optional_params": ["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. host_face must provide a strict selector or an explicit frame.", "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", "host_face"], "optional_params": ["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. host_face must provide a strict selector or an explicit frame.", "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}, - "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}, + "hole_blind": {"summary": "Cut one or more blind cylindrical holes in the current body.", "required_params": ["diameter_mm", "depth_mm", "positions", "host_face"], "optional_params": ["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. host_face is injected from exactly one face selector token.", "requires_sketch": false}, + "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", "host_face"], "optional_params": ["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. host_face is injected from exactly one face selector token.", "requires_sketch": false}, + "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", "host_face"], "optional_params": ["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. host_face is injected from exactly one face selector token.", "requires_sketch": false}, + "sphere_add": {"summary": "Add one spherical solid at an explicit model-space center.", "required_params": ["radius_mm", "center_mm"], "optional_params": [], "requires_sketch": true, "produces_body": true}, + "fillet": {"summary": "Apply a radius to selected edges or faces.", "required_params": ["radius_mm"], "optional_params": ["tangent_propagation"], "requires_sketch": false, "selector_slot": {"path": "feature.selectors", "min_items": 1, "max_items": 64}}, + "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, "selector_slot": {"path": "feature.selectors", "min_items": 1, "max_items": 64}}, "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}, - "pattern_mirror": {"summary": "Mirror source features about a selected plane.", "required_params": ["source_feature_ids", "mirror_plane"], "optional_params": [], "requires_sketch": false}, + "pattern_mirror": {"summary": "Mirror source features about a selected plane.", "required_params": ["source_feature_ids", "mirror_plane"], "optional_params": [], "requires_sketch": false, "selector_slot": {"path": "params.mirror_plane", "min_items": 1, "max_items": 1}}, "reference_plane": {"summary": "A named reference plane used by sketches or patterns.", "required_params": ["plane"], "optional_params": [], "requires_sketch": false}, "reference_axis": {"summary": "A named reference axis used by revolve or pattern features.", "required_params": ["axis"], "optional_params": [], "requires_sketch": false}, - "hole_wizard": {"summary": "A SolidWorks Hole Wizard feature including its typed dimensional contract and placement selectors.", "required_params": ["hole_type", "diameter_mm", "depth_mm"], "optional_params": ["positions", "host_face", "thread", "countersink", "counterbore"], "requires_sketch": false} + "hole_wizard": {"summary": "A SolidWorks Hole Wizard feature including its typed dimensional contract and placement selectors.", "required_params": ["hole_type", "diameter_mm", "depth_mm"], "optional_params": ["positions", "host_face", "thread", "countersink", "counterbore"], "requires_sketch": false, "selector_slot": {"path": "params.host_face", "min_items": 1, "max_items": 1}} }, "profiles": { "circle": { diff --git a/backend/engine/cdsl_engine/runtime.py b/backend/engine/cdsl_engine/runtime.py index 5aaae5c8..ec1d5964 100644 --- a/backend/engine/cdsl_engine/runtime.py +++ b/backend/engine/cdsl_engine/runtime.py @@ -549,9 +549,13 @@ def _execute_hole(node: FeaturePlanNode, session: ExecutionSession, *, wizard: b positions_are_local = False # 3. 解析孔规格 HoleSpec(直径、深度、类型等,wizard 模式提供额外默认值)。 spec = HoleSpec.from_feature(node.atomic_id, node.params, wizard=wizard) - # 4. 确定孔轴向:默认沿宿主面法向,但需保证指向主体内部(按主体中心与面原点的相对位置取反)。 - normal = host.normal - inward = normal if vector_dot(vector_subtract(session.adapter.body_center(session.body), host.origin_mm), normal) >= 0 else vector_scale(normal, -1) + # 4. A host-face normal is an outward B-rep orientation, so its inverse + # always enters the material. Inferring direction from the global body + # centre fails for concave or multi-leg parts: for example, the top face + # of an L bracket can sit below the whole body's centre and the old rule + # drilled outward, producing a no-op feature reported as successful. + # The selected topology face is the local, authoritative orientation. + inward = vector_scale(host.normal, -1) # 5. 生成孔切除工具:按孔规格、起始位置、内方向及“贯穿到主体底面”的深度构造工具实体。 tool = session.adapter.hole_tool( spec,