solidworks-mcp-python 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- solidworks_mcp/__init__.py +57 -0
- solidworks_mcp/adapters/__init__.py +40 -0
- solidworks_mcp/adapters/base.py +1882 -0
- solidworks_mcp/adapters/circuit_breaker.py +2052 -0
- solidworks_mcp/adapters/com_executor.py +252 -0
- solidworks_mcp/adapters/complexity_analyzer.py +272 -0
- solidworks_mcp/adapters/connection_pool.py +1884 -0
- solidworks_mcp/adapters/factory.py +366 -0
- solidworks_mcp/adapters/intelligent_router.py +218 -0
- solidworks_mcp/adapters/mock_adapter.py +4016 -0
- solidworks_mcp/adapters/pywin32_adapter.py +2842 -0
- solidworks_mcp/adapters/solidworks/__init__.py +17 -0
- solidworks_mcp/adapters/solidworks/bitmap.py +180 -0
- solidworks_mcp/adapters/solidworks/com_utils.py +56 -0
- solidworks_mcp/adapters/solidworks/dimension_layout.py +753 -0
- solidworks_mcp/adapters/solidworks/documentation.py +1476 -0
- solidworks_mcp/adapters/solidworks/drawing_standard.py +1993 -0
- solidworks_mcp/adapters/solidworks/face_selection.py +392 -0
- solidworks_mcp/adapters/solidworks/features.py +3362 -0
- solidworks_mcp/adapters/solidworks/io.py +3649 -0
- solidworks_mcp/adapters/solidworks/rebuild_diagnostics.py +269 -0
- solidworks_mcp/adapters/solidworks/rebuild_inspector.py +146 -0
- solidworks_mcp/adapters/solidworks/selection.py +308 -0
- solidworks_mcp/adapters/solidworks/sketch.py +2545 -0
- solidworks_mcp/adapters/sw_feature_errors.py +66 -0
- solidworks_mcp/adapters/sw_type_info.py +317 -0
- solidworks_mcp/adapters/vba_adapter.py +318 -0
- solidworks_mcp/adapters/vba_macro_executor.py +215 -0
- solidworks_mcp/agents/__init__.py +68 -0
- solidworks_mcp/agents/design_knowledge/assembly_patterns.md +127 -0
- solidworks_mcp/agents/design_knowledge/hinges.md +118 -0
- solidworks_mcp/agents/design_knowledge/snap_fits.md +93 -0
- solidworks_mcp/agents/design_knowledge/solidworks_basics.md +3 -0
- solidworks_mcp/agents/design_knowledge/tolerancing.md +128 -0
- solidworks_mcp/agents/harness.py +216 -0
- solidworks_mcp/agents/history_db.py +1508 -0
- solidworks_mcp/agents/retrieval_index.py +164 -0
- solidworks_mcp/agents/schemas.py +165 -0
- solidworks_mcp/agents/smoke_test.py +285 -0
- solidworks_mcp/agents/soc_exporter.py +650 -0
- solidworks_mcp/agents/soc_pickup.py +332 -0
- solidworks_mcp/agents/soc_rewind.py +221 -0
- solidworks_mcp/agents/vector_rag.py +665 -0
- solidworks_mcp/cache/__init__.py +5 -0
- solidworks_mcp/cache/response_cache.py +164 -0
- solidworks_mcp/config.py +729 -0
- solidworks_mcp/exceptions.py +37 -0
- solidworks_mcp/security/__init__.py +68 -0
- solidworks_mcp/security/auth.py +128 -0
- solidworks_mcp/security/cors.py +27 -0
- solidworks_mcp/security/rate_limiting.py +90 -0
- solidworks_mcp/security/runtime.py +155 -0
- solidworks_mcp/server.py +777 -0
- solidworks_mcp/server_cli_fixed.py +120 -0
- solidworks_mcp/setup/__init__.py +1 -0
- solidworks_mcp/setup/cli.py +399 -0
- solidworks_mcp/setup/clients.py +157 -0
- solidworks_mcp/setup/config_file.py +230 -0
- solidworks_mcp/setup/handshake.py +202 -0
- solidworks_mcp/setup/launcher.py +102 -0
- solidworks_mcp/setup/legacy.py +100 -0
- solidworks_mcp/setup/registration.py +193 -0
- solidworks_mcp/setup/solidworks.py +49 -0
- solidworks_mcp/tools/__init__.py +64 -0
- solidworks_mcp/tools/analysis.py +723 -0
- solidworks_mcp/tools/automation.py +578 -0
- solidworks_mcp/tools/docs_discovery.py +1476 -0
- solidworks_mcp/tools/drawing.py +1015 -0
- solidworks_mcp/tools/drawing_analysis.py +456 -0
- solidworks_mcp/tools/drawing_automation.py +627 -0
- solidworks_mcp/tools/engineering_data.py +466 -0
- solidworks_mcp/tools/error_classifier.py +232 -0
- solidworks_mcp/tools/export.py +833 -0
- solidworks_mcp/tools/file_facts.py +40 -0
- solidworks_mcp/tools/file_management.py +1473 -0
- solidworks_mcp/tools/input_compat.py +17 -0
- solidworks_mcp/tools/macro_recording.py +464 -0
- solidworks_mcp/tools/modeling.py +2360 -0
- solidworks_mcp/tools/sketching.py +1422 -0
- solidworks_mcp/tools/template_management.py +367 -0
- solidworks_mcp/tools/tool_boundary.py +280 -0
- solidworks_mcp/tools/vba_generation.py +1145 -0
- solidworks_mcp/ui/__init__.py +9 -0
- solidworks_mcp/ui/local_llm.py +725 -0
- solidworks_mcp/ui/routers/__init__.py +10 -0
- solidworks_mcp/ui/routers/checkpoint.py +24 -0
- solidworks_mcp/ui/routers/docs.py +47 -0
- solidworks_mcp/ui/routers/llm.py +83 -0
- solidworks_mcp/ui/routers/local_model.py +69 -0
- solidworks_mcp/ui/routers/model.py +75 -0
- solidworks_mcp/ui/routers/preview.py +38 -0
- solidworks_mcp/ui/routers/session.py +161 -0
- solidworks_mcp/ui/routers/viewer.py +154 -0
- solidworks_mcp/ui/schemas.py +502 -0
- solidworks_mcp/ui/server.py +298 -0
- solidworks_mcp/ui/service.py +481 -0
- solidworks_mcp/ui/services/__init__.py +136 -0
- solidworks_mcp/ui/services/_utils.py +958 -0
- solidworks_mcp/ui/services/checkpoint_service.py +1110 -0
- solidworks_mcp/ui/services/docs_service.py +282 -0
- solidworks_mcp/ui/services/llm_service.py +956 -0
- solidworks_mcp/ui/services/model_service.py +459 -0
- solidworks_mcp/ui/services/parameter_repair_service.py +314 -0
- solidworks_mcp/ui/services/preview_service.py +489 -0
- solidworks_mcp/ui/services/session_service.py +1389 -0
- solidworks_mcp/utils/__init__.py +9 -0
- solidworks_mcp/utils/feature_tree_classifier.py +342 -0
- solidworks_mcp/utils/logging.py +76 -0
- solidworks_mcp/utils/validation.py +77 -0
- solidworks_mcp/version.py +8 -0
- solidworks_mcp_python-1.5.0.dist-info/METADATA +410 -0
- solidworks_mcp_python-1.5.0.dist-info/RECORD +115 -0
- solidworks_mcp_python-1.5.0.dist-info/WHEEL +4 -0
- solidworks_mcp_python-1.5.0.dist-info/entry_points.txt +3 -0
- solidworks_mcp_python-1.5.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1110 @@
|
|
|
1
|
+
"""Checkpoint execution service for the Prefab CAD assistant dashboard.
|
|
2
|
+
|
|
3
|
+
Responsible for planning and executing individual checkpoint steps against the
|
|
4
|
+
SolidWorks adapter.
|
|
5
|
+
|
|
6
|
+
Design note:
|
|
7
|
+
``_run_checkpoint_tools`` uses a handler map to dispatch tool names to adapter
|
|
8
|
+
calls, allowing new tool bindings without growing a long conditional chain.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from textwrap import dedent
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from ...adapters import create_adapter
|
|
20
|
+
from ...agents.history_db import (
|
|
21
|
+
insert_tool_call_record,
|
|
22
|
+
list_plan_checkpoints,
|
|
23
|
+
update_plan_checkpoint,
|
|
24
|
+
upsert_design_session,
|
|
25
|
+
)
|
|
26
|
+
from ...config import load_config
|
|
27
|
+
from ._utils import (
|
|
28
|
+
DEFAULT_API_ORIGIN,
|
|
29
|
+
DEFAULT_PREVIEW_ORIENTATION,
|
|
30
|
+
DEFAULT_SESSION_ID,
|
|
31
|
+
DEFAULT_SOURCE_MODE,
|
|
32
|
+
DEFAULT_USER_GOAL,
|
|
33
|
+
ensure_preview_dir,
|
|
34
|
+
merge_metadata,
|
|
35
|
+
parse_json_blob,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Internal helpers
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
def _planned_tools(planned: dict[str, Any]) -> list[str]:
|
|
43
|
+
"""Return the list of tool names from a planned-action dict.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
planned: Parsed planned-action JSON dict.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
List of tool name strings.
|
|
50
|
+
"""
|
|
51
|
+
tools = planned.get("tools", [])
|
|
52
|
+
return [str(t) for t in tools] if isinstance(tools, list) else []
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _checkpoint_script_dir(session_id: str) -> Path:
|
|
56
|
+
return Path(".solidworks_mcp") / "checkpoint_scripts" / session_id
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _checkpoint_script_path(session_id: str, checkpoint_index: int) -> Path:
|
|
60
|
+
stamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S_%f")
|
|
61
|
+
script_name = f"checkpoint_{checkpoint_index:03d}_{stamp}.py"
|
|
62
|
+
return _checkpoint_script_dir(session_id) / script_name
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _planned_tool_payloads(planned: dict[str, Any], tool: str) -> list[dict[str, Any]]:
|
|
66
|
+
"""Return ordered payload dicts for a tool from planned action JSON.
|
|
67
|
+
|
|
68
|
+
Supports either flat payloads (top-level keys) or nested execution-style
|
|
69
|
+
payloads stored under ``tool`` and ``tool#N`` keys.
|
|
70
|
+
"""
|
|
71
|
+
payloads: list[dict[str, Any]] = []
|
|
72
|
+
direct = planned.get(tool)
|
|
73
|
+
if isinstance(direct, dict):
|
|
74
|
+
payloads.append(dict(direct))
|
|
75
|
+
|
|
76
|
+
suffixed: list[tuple[str, dict[str, Any]]] = []
|
|
77
|
+
for key, value in planned.items():
|
|
78
|
+
if not isinstance(key, str) or not isinstance(value, dict):
|
|
79
|
+
continue
|
|
80
|
+
if key.startswith(f"{tool}#"):
|
|
81
|
+
suffixed.append((key, dict(value)))
|
|
82
|
+
|
|
83
|
+
def _suffix_order(item: tuple[str, dict[str, Any]]) -> tuple[int, str]:
|
|
84
|
+
key = item[0]
|
|
85
|
+
suffix = key.split("#", 1)[1] if "#" in key else ""
|
|
86
|
+
return (int(suffix) if suffix.isdigit() else 10_000, key)
|
|
87
|
+
|
|
88
|
+
for _, payload in sorted(suffixed, key=_suffix_order):
|
|
89
|
+
payloads.append(payload)
|
|
90
|
+
|
|
91
|
+
return payloads
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _should_open_empty_part(
|
|
95
|
+
session_meta: dict[str, Any], planned: dict[str, Any]
|
|
96
|
+
) -> bool:
|
|
97
|
+
"""Return whether checkpoint execution should open a blank part first."""
|
|
98
|
+
workflow_mode = str(session_meta.get("workflow_mode") or "").strip().lower()
|
|
99
|
+
if workflow_mode != "new_design":
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
if str(session_meta.get("active_model_path") or "").strip():
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
if bool(session_meta.get("new_design_part_opened")):
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
tools = _planned_tools(planned)
|
|
109
|
+
return not any(
|
|
110
|
+
tool in {"create_part", "create_assembly", "open_model"} for tool in tools
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def _open_empty_part_before_checkpoint(
|
|
115
|
+
*,
|
|
116
|
+
session_id: str,
|
|
117
|
+
session_row: dict[str, Any],
|
|
118
|
+
db_path: Path | None,
|
|
119
|
+
api_origin: str,
|
|
120
|
+
) -> dict[str, Any]:
|
|
121
|
+
"""Create a blank part document and refresh preview before checkpoint execution."""
|
|
122
|
+
from .preview_service import refresh_preview # noqa: PLC0415
|
|
123
|
+
|
|
124
|
+
config = load_config()
|
|
125
|
+
adapter = await create_adapter(config)
|
|
126
|
+
part_name = f"{session_id}_new_part"
|
|
127
|
+
try:
|
|
128
|
+
await adapter.connect()
|
|
129
|
+
create_result = await adapter.create_part(name=part_name)
|
|
130
|
+
if not getattr(create_result, "is_success", False):
|
|
131
|
+
raise RuntimeError(create_result.error or "Failed to open blank part")
|
|
132
|
+
|
|
133
|
+
merge_metadata(
|
|
134
|
+
session_id,
|
|
135
|
+
db_path=db_path,
|
|
136
|
+
workflow_mode="new_design",
|
|
137
|
+
new_design_part_opened=True,
|
|
138
|
+
new_design_part_name=part_name,
|
|
139
|
+
active_model_status="Opened a new empty part document for checkpoint execution.",
|
|
140
|
+
active_model_type="Part",
|
|
141
|
+
active_model_configuration="Default",
|
|
142
|
+
preview_status="Refreshing preview for new empty part...",
|
|
143
|
+
latest_message="Opened a new empty part document for checkpoint execution.",
|
|
144
|
+
latest_error_text="",
|
|
145
|
+
remediation_hint="",
|
|
146
|
+
)
|
|
147
|
+
insert_tool_call_record(
|
|
148
|
+
session_id=session_id,
|
|
149
|
+
tool_name="ui.open_empty_part",
|
|
150
|
+
input_json=json.dumps(
|
|
151
|
+
{
|
|
152
|
+
"session_id": session_id,
|
|
153
|
+
"part_name": part_name,
|
|
154
|
+
"user_goal": session_row.get("user_goal") or DEFAULT_USER_GOAL,
|
|
155
|
+
},
|
|
156
|
+
ensure_ascii=True,
|
|
157
|
+
),
|
|
158
|
+
output_json=json.dumps(
|
|
159
|
+
{"part_name": part_name, "status": "success"},
|
|
160
|
+
ensure_ascii=True,
|
|
161
|
+
),
|
|
162
|
+
success=True,
|
|
163
|
+
db_path=db_path,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
await refresh_preview(
|
|
168
|
+
session_id,
|
|
169
|
+
orientation=DEFAULT_PREVIEW_ORIENTATION,
|
|
170
|
+
db_path=db_path,
|
|
171
|
+
preview_dir=ensure_preview_dir(),
|
|
172
|
+
api_origin=api_origin,
|
|
173
|
+
adapter_override=adapter,
|
|
174
|
+
active_model_path_override="",
|
|
175
|
+
reopen_active_model=False,
|
|
176
|
+
)
|
|
177
|
+
except Exception as preview_exc:
|
|
178
|
+
merge_metadata(
|
|
179
|
+
session_id,
|
|
180
|
+
db_path=db_path,
|
|
181
|
+
latest_message="Opened a new empty part document; preview refresh is still pending.",
|
|
182
|
+
preview_status="Opened a new empty part document; preview refresh is still pending.",
|
|
183
|
+
latest_error_text=str(preview_exc),
|
|
184
|
+
remediation_hint="Retry preview refresh after SolidWorks finishes opening the new part.",
|
|
185
|
+
)
|
|
186
|
+
finally:
|
|
187
|
+
try:
|
|
188
|
+
await adapter.disconnect()
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
return {"part_name": part_name, "status": "success"}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _render_checkpoint_script(
|
|
196
|
+
planned: dict[str, Any],
|
|
197
|
+
*,
|
|
198
|
+
session_id: str,
|
|
199
|
+
checkpoint_index: int,
|
|
200
|
+
checkpoint_title: str,
|
|
201
|
+
) -> str:
|
|
202
|
+
"""Render a strict script for checkpoint execution.
|
|
203
|
+
|
|
204
|
+
The generated script has explicit breakpoint comments and does not use default
|
|
205
|
+
geometry fallbacks. Missing parameters raise errors so users can repair the plan.
|
|
206
|
+
"""
|
|
207
|
+
planned_json = json.dumps(planned, ensure_ascii=True)
|
|
208
|
+
return dedent(
|
|
209
|
+
f"""
|
|
210
|
+
from __future__ import annotations
|
|
211
|
+
|
|
212
|
+
import asyncio
|
|
213
|
+
import json
|
|
214
|
+
import os
|
|
215
|
+
import pdb
|
|
216
|
+
import traceback
|
|
217
|
+
from typing import Any
|
|
218
|
+
|
|
219
|
+
from solidworks_mcp.adapters import create_adapter
|
|
220
|
+
from solidworks_mcp.adapters.base import ExtrusionParameters
|
|
221
|
+
from solidworks_mcp.config import load_config
|
|
222
|
+
|
|
223
|
+
SESSION_ID = {session_id!r}
|
|
224
|
+
CHECKPOINT_INDEX = {checkpoint_index}
|
|
225
|
+
CHECKPOINT_TITLE = {checkpoint_title!r}
|
|
226
|
+
PLANNED: dict[str, Any] = json.loads({planned_json!r})
|
|
227
|
+
DEBUG_PAUSE = os.getenv("SOLIDWORKS_UI_CHECKPOINT_DEBUG_PAUSE", "0").strip().lower() in {"1", "true", "yes", "on"}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def require_key(key: str) -> Any:
|
|
231
|
+
if key not in PLANNED:
|
|
232
|
+
raise ValueError(f"Missing required planned key: {{key}}")
|
|
233
|
+
return PLANNED[key]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def require_float(key: str) -> float:
|
|
237
|
+
value = require_key(key)
|
|
238
|
+
try:
|
|
239
|
+
return float(value)
|
|
240
|
+
except (TypeError, ValueError) as exc:
|
|
241
|
+
raise ValueError(f"Invalid numeric value for {{key}}: {{value!r}}") from exc
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def require_first_float(*keys: str) -> float:
|
|
245
|
+
for key in keys:
|
|
246
|
+
if key in PLANNED:
|
|
247
|
+
try:
|
|
248
|
+
return float(PLANNED[key])
|
|
249
|
+
except (TypeError, ValueError) as exc:
|
|
250
|
+
raise ValueError(
|
|
251
|
+
f"Invalid numeric value for {{key}}: {{PLANNED[key]!r}}"
|
|
252
|
+
) from exc
|
|
253
|
+
raise ValueError(f"Missing required planned key(s): {{', '.join(keys)}}")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def require_vec(key: str, size: int) -> list[float]:
|
|
257
|
+
raw = require_key(key)
|
|
258
|
+
if not isinstance(raw, list) or len(raw) != size:
|
|
259
|
+
raise ValueError(
|
|
260
|
+
f"{{key}} must be a list with {{size}} numeric values; got {{raw!r}}"
|
|
261
|
+
)
|
|
262
|
+
try:
|
|
263
|
+
return [float(v) for v in raw]
|
|
264
|
+
except (TypeError, ValueError) as exc:
|
|
265
|
+
raise ValueError(f"{{key}} must contain numeric values; got {{raw!r}}") from exc
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def require_result(result: Any, label: str) -> Any:
|
|
269
|
+
if result is None:
|
|
270
|
+
raise RuntimeError(f"{{label}} returned None")
|
|
271
|
+
if not getattr(result, "is_success", False):
|
|
272
|
+
raise RuntimeError(f"{{label}} failed: {{getattr(result, 'error', 'unknown error')}}")
|
|
273
|
+
return result
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def pause_point(label: str) -> None:
|
|
277
|
+
print(f"EDIT POINT: {{label}}")
|
|
278
|
+
if DEBUG_PAUSE:
|
|
279
|
+
pdb.set_trace()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def payloads_for_tool(tool: str) -> list[dict[str, Any]]:
|
|
283
|
+
payloads: list[dict[str, Any]] = []
|
|
284
|
+
direct = PLANNED.get(tool)
|
|
285
|
+
if isinstance(direct, dict):
|
|
286
|
+
payloads.append(dict(direct))
|
|
287
|
+
|
|
288
|
+
suffixed: list[tuple[str, dict[str, Any]]] = []
|
|
289
|
+
for key, value in PLANNED.items():
|
|
290
|
+
if not isinstance(key, str) or not isinstance(value, dict):
|
|
291
|
+
continue
|
|
292
|
+
if key.startswith(f"{{tool}}#"):
|
|
293
|
+
suffixed.append((key, dict(value)))
|
|
294
|
+
|
|
295
|
+
def suffix_order(item: tuple[str, dict[str, Any]]) -> tuple[int, str]:
|
|
296
|
+
key = item[0]
|
|
297
|
+
suffix = key.split("#", 1)[1] if "#" in key else ""
|
|
298
|
+
return (int(suffix) if suffix.isdigit() else 10_000, key)
|
|
299
|
+
|
|
300
|
+
for _, payload in sorted(suffixed, key=suffix_order):
|
|
301
|
+
payloads.append(payload)
|
|
302
|
+
|
|
303
|
+
return payloads
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def key_from(payload: dict[str, Any], key: str) -> Any:
|
|
307
|
+
if key in payload:
|
|
308
|
+
return payload[key]
|
|
309
|
+
return require_key(key)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def vec_from(payload: dict[str, Any], key: str, size: int) -> list[float]:
|
|
313
|
+
raw = key_from(payload, key)
|
|
314
|
+
if not isinstance(raw, list) or len(raw) != size:
|
|
315
|
+
raise ValueError(
|
|
316
|
+
f"{{key}} must be a list with {{size}} numeric values; got {{raw!r}}"
|
|
317
|
+
)
|
|
318
|
+
try:
|
|
319
|
+
return [float(v) for v in raw]
|
|
320
|
+
except (TypeError, ValueError) as exc:
|
|
321
|
+
raise ValueError(f"{{key}} must contain numeric values; got {{raw!r}}") from exc
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def first_float_from(payload: dict[str, Any], *keys: str) -> float:
|
|
325
|
+
for key in keys:
|
|
326
|
+
if key in payload:
|
|
327
|
+
try:
|
|
328
|
+
return float(payload[key])
|
|
329
|
+
except (TypeError, ValueError) as exc:
|
|
330
|
+
raise ValueError(f"Invalid numeric value for {{key}}: {{payload[key]!r}}") from exc
|
|
331
|
+
return require_first_float(*keys)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
async def run_checkpoint() -> dict[str, Any]:
|
|
335
|
+
tools = PLANNED.get("tools", [])
|
|
336
|
+
if not isinstance(tools, list) or not tools:
|
|
337
|
+
raise ValueError("planned.tools must be a non-empty list")
|
|
338
|
+
|
|
339
|
+
tool_runs: list[dict[str, str]] = []
|
|
340
|
+
failed_tools: list[str] = []
|
|
341
|
+
|
|
342
|
+
config = load_config()
|
|
343
|
+
adapter = await create_adapter(config)
|
|
344
|
+
|
|
345
|
+
await adapter.connect()
|
|
346
|
+
try:
|
|
347
|
+
for idx, tool in enumerate([str(t) for t in tools], start=1):
|
|
348
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} step {{idx}}/{{len(tools)}} before {{tool}}")
|
|
349
|
+
print(f"CHECKPOINT {{CHECKPOINT_INDEX}} STEP {{idx}}/{{len(tools)}}: {{tool}}")
|
|
350
|
+
try:
|
|
351
|
+
if tool == "create_part":
|
|
352
|
+
payload = (payloads_for_tool(tool) or [{{}}])[0]
|
|
353
|
+
part_name = str(key_from(payload, "part_name"))
|
|
354
|
+
require_result(await adapter.create_part(name=part_name), "create_part")
|
|
355
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": f"Created part '{{part_name}}'"}})
|
|
356
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after create_part")
|
|
357
|
+
|
|
358
|
+
elif tool == "create_assembly":
|
|
359
|
+
payload = (payloads_for_tool(tool) or [{{}}])[0]
|
|
360
|
+
assembly_name = str(key_from(payload, "assembly_name"))
|
|
361
|
+
require_result(await adapter.create_assembly(name=assembly_name), "create_assembly")
|
|
362
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": f"Created assembly '{{assembly_name}}'"}})
|
|
363
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after create_assembly")
|
|
364
|
+
|
|
365
|
+
elif tool == "open_model":
|
|
366
|
+
payload = (payloads_for_tool(tool) or [{{}}])[0]
|
|
367
|
+
model_path = str(key_from(payload, "model_path"))
|
|
368
|
+
require_result(await adapter.open_model(model_path), "open_model")
|
|
369
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": f"Opened model '{{model_path}}'"}})
|
|
370
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after open_model")
|
|
371
|
+
|
|
372
|
+
elif tool == "create_sketch":
|
|
373
|
+
payloads = payloads_for_tool(tool) or [{{}}]
|
|
374
|
+
for payload in payloads:
|
|
375
|
+
sketch_plane = str(key_from(payload, "sketch_plane"))
|
|
376
|
+
require_result(await adapter.create_sketch(sketch_plane), "create_sketch")
|
|
377
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": f"Created sketch on '{{sketch_plane}}'"}})
|
|
378
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after create_sketch")
|
|
379
|
+
|
|
380
|
+
elif tool == "exit_sketch":
|
|
381
|
+
require_result(await adapter.exit_sketch(), "exit_sketch")
|
|
382
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": "Exited sketch"}})
|
|
383
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after exit_sketch")
|
|
384
|
+
|
|
385
|
+
elif tool == "add_line":
|
|
386
|
+
payloads = payloads_for_tool(tool) or [{{}}]
|
|
387
|
+
for payload in payloads:
|
|
388
|
+
if "line_mm" in payload or "line_mm" in PLANNED:
|
|
389
|
+
x1, y1, x2, y2 = vec_from(payload, "line_mm", 4)
|
|
390
|
+
else:
|
|
391
|
+
sx, sy = vec_from(payload, "line_start_mm", 2)
|
|
392
|
+
ex, ey = vec_from(payload, "line_end_mm", 2)
|
|
393
|
+
x1, y1, x2, y2 = sx, sy, ex, ey
|
|
394
|
+
require_result(await adapter.add_line(x1, y1, x2, y2), "add_line")
|
|
395
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": "Added line"}})
|
|
396
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after add_line")
|
|
397
|
+
|
|
398
|
+
elif tool == "add_rectangle":
|
|
399
|
+
payloads = payloads_for_tool(tool) or [{{}}]
|
|
400
|
+
for payload in payloads:
|
|
401
|
+
x, y, width, height = vec_from(payload, "rectangle_mm", 4)
|
|
402
|
+
require_result(await adapter.add_rectangle(x, y, width, height), "add_rectangle")
|
|
403
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": "Added rectangle"}})
|
|
404
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after add_rectangle")
|
|
405
|
+
|
|
406
|
+
elif tool == "add_circle":
|
|
407
|
+
payloads = payloads_for_tool(tool) or [{{}}]
|
|
408
|
+
for payload in payloads:
|
|
409
|
+
cx, cy = vec_from(payload, "circle_center_mm", 2)
|
|
410
|
+
radius = first_float_from(payload, "circle_radius_mm")
|
|
411
|
+
require_result(await adapter.add_circle(cx, cy, radius), "add_circle")
|
|
412
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": "Added circle"}})
|
|
413
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after add_circle")
|
|
414
|
+
|
|
415
|
+
elif tool == "add_centerline":
|
|
416
|
+
payloads = payloads_for_tool(tool) or [{{}}]
|
|
417
|
+
for payload in payloads:
|
|
418
|
+
x1, y1, x2, y2 = vec_from(payload, "centerline_mm", 4)
|
|
419
|
+
require_result(await adapter.add_centerline(x1, y1, x2, y2), "add_centerline")
|
|
420
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": "Added centerline"}})
|
|
421
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after add_centerline")
|
|
422
|
+
|
|
423
|
+
elif tool == "add_arc":
|
|
424
|
+
payloads = payloads_for_tool(tool) or [{{}}]
|
|
425
|
+
for payload in payloads:
|
|
426
|
+
cx, cy = vec_from(payload, "arc_center_mm", 2)
|
|
427
|
+
sx, sy = vec_from(payload, "arc_start_mm", 2)
|
|
428
|
+
ex, ey = vec_from(payload, "arc_end_mm", 2)
|
|
429
|
+
require_result(await adapter.add_arc(cx, cy, sx, sy, ex, ey), "add_arc")
|
|
430
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": "Added arc"}})
|
|
431
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after add_arc")
|
|
432
|
+
|
|
433
|
+
elif tool == "create_extrusion":
|
|
434
|
+
payload = (payloads_for_tool(tool) or [{{}}])[0]
|
|
435
|
+
depth = first_float_from(payload, "depth_mm", "depth")
|
|
436
|
+
params = ExtrusionParameters(
|
|
437
|
+
depth=depth,
|
|
438
|
+
thin_feature=bool(payload.get("thin_feature", PLANNED.get("thin_feature", False))),
|
|
439
|
+
thin_thickness=(
|
|
440
|
+
float(payload["thin_thickness_mm"])
|
|
441
|
+
if "thin_thickness_mm" in payload
|
|
442
|
+
else (
|
|
443
|
+
float(PLANNED["thin_thickness_mm"])
|
|
444
|
+
if "thin_thickness_mm" in PLANNED
|
|
445
|
+
else float(payload.get("thin_thickness", PLANNED.get("thin_thickness", 0.0)))
|
|
446
|
+
)
|
|
447
|
+
),
|
|
448
|
+
both_directions=bool(payload.get("both_directions", PLANNED.get("both_directions", False))),
|
|
449
|
+
)
|
|
450
|
+
require_result(await adapter.create_extrusion(params), "create_extrusion")
|
|
451
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": f"Created extrusion depth={{depth}}mm"}})
|
|
452
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after create_extrusion")
|
|
453
|
+
|
|
454
|
+
elif tool == "create_cut_extrude":
|
|
455
|
+
payload = (payloads_for_tool(tool) or [{{}}])[0]
|
|
456
|
+
depth = first_float_from(payload, "depth_mm", "depth")
|
|
457
|
+
params = ExtrusionParameters(depth=depth)
|
|
458
|
+
require_result(await adapter.create_cut_extrude(params), "create_cut_extrude")
|
|
459
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": f"Created cut-extrude depth={{depth}}mm"}})
|
|
460
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after create_cut_extrude")
|
|
461
|
+
|
|
462
|
+
elif tool == "create_cut":
|
|
463
|
+
payload = (payloads_for_tool(tool) or [{{}}])[0]
|
|
464
|
+
depth = first_float_from(payload, "depth_mm", "depth")
|
|
465
|
+
sketch_name = str(payload.get("sketch_name", PLANNED.get("sketch_name", "")))
|
|
466
|
+
if sketch_name:
|
|
467
|
+
require_result(await adapter.create_cut(sketch_name, depth), "create_cut")
|
|
468
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": f"Created cut from {{sketch_name}} depth={{depth}}mm"}})
|
|
469
|
+
else:
|
|
470
|
+
params = ExtrusionParameters(depth=depth)
|
|
471
|
+
require_result(await adapter.create_cut_extrude(params), "create_cut")
|
|
472
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": f"Created cut-extrude depth={{depth}}mm"}})
|
|
473
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after create_cut")
|
|
474
|
+
|
|
475
|
+
elif tool == "add_fillet":
|
|
476
|
+
payload = (payloads_for_tool(tool) or [{{}}])[0]
|
|
477
|
+
radius = first_float_from(payload, "radius_mm", "radius")
|
|
478
|
+
raw_edges = payload.get("edge_names", PLANNED.get("edge_names", []))
|
|
479
|
+
edge_names = [str(edge) for edge in raw_edges] if isinstance(raw_edges, list) else []
|
|
480
|
+
require_result(await adapter.add_fillet(radius, edge_names), "add_fillet")
|
|
481
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": f"Added fillet radius={{radius}}mm"}})
|
|
482
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after add_fillet")
|
|
483
|
+
|
|
484
|
+
elif tool == "check_sketch_fully_defined":
|
|
485
|
+
payload = (payloads_for_tool(tool) or [{{}}])[0]
|
|
486
|
+
sketch_name = payload.get("sketch_name", PLANNED.get("sketch_name"))
|
|
487
|
+
require_result(
|
|
488
|
+
await adapter.check_sketch_fully_defined(
|
|
489
|
+
str(sketch_name) if sketch_name else None
|
|
490
|
+
),
|
|
491
|
+
"check_sketch_fully_defined",
|
|
492
|
+
)
|
|
493
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": "Checked sketch definition"}})
|
|
494
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after check_sketch_fully_defined")
|
|
495
|
+
|
|
496
|
+
elif tool == "save_file":
|
|
497
|
+
payload = (payloads_for_tool(tool) or [{{}}])[0]
|
|
498
|
+
file_path = str(key_from(payload, "file_path"))
|
|
499
|
+
require_result(await adapter.save_file(file_path), "save_file")
|
|
500
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": f"Saved file '{{file_path}}'"}})
|
|
501
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after save_file")
|
|
502
|
+
|
|
503
|
+
elif tool == "get_model_info":
|
|
504
|
+
require_result(await adapter.get_model_info(), "get_model_info")
|
|
505
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": "Retrieved model info"}})
|
|
506
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after get_model_info")
|
|
507
|
+
|
|
508
|
+
elif tool == "list_features":
|
|
509
|
+
require_result(
|
|
510
|
+
await adapter.list_features(include_suppressed=True),
|
|
511
|
+
"list_features",
|
|
512
|
+
)
|
|
513
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": "Listed features"}})
|
|
514
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after list_features")
|
|
515
|
+
|
|
516
|
+
elif tool == "get_mass_properties":
|
|
517
|
+
require_result(await adapter.get_mass_properties(), "get_mass_properties")
|
|
518
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": "Retrieved mass properties"}})
|
|
519
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after get_mass_properties")
|
|
520
|
+
|
|
521
|
+
elif tool == "analyze_geometry":
|
|
522
|
+
require_result(await adapter.get_model_info(), "analyze_geometry.get_model_info")
|
|
523
|
+
require_result(
|
|
524
|
+
await adapter.list_features(include_suppressed=True),
|
|
525
|
+
"analyze_geometry.list_features",
|
|
526
|
+
)
|
|
527
|
+
require_result(
|
|
528
|
+
await adapter.get_mass_properties(),
|
|
529
|
+
"analyze_geometry.get_mass_properties",
|
|
530
|
+
)
|
|
531
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": "Geometry analysis completed"}})
|
|
532
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after analyze_geometry")
|
|
533
|
+
|
|
534
|
+
elif tool == "export_image":
|
|
535
|
+
payloads = payloads_for_tool(tool)
|
|
536
|
+
payload = payloads[0] if payloads else PLANNED.get("export_image")
|
|
537
|
+
if payload is None:
|
|
538
|
+
payload = {{
|
|
539
|
+
"file_path": require_key("file_path"),
|
|
540
|
+
"format_type": require_key("format_type"),
|
|
541
|
+
}}
|
|
542
|
+
if "width" in PLANNED:
|
|
543
|
+
payload["width"] = int(PLANNED["width"])
|
|
544
|
+
if "height" in PLANNED:
|
|
545
|
+
payload["height"] = int(PLANNED["height"])
|
|
546
|
+
if "view_orientation" in PLANNED:
|
|
547
|
+
payload["view_orientation"] = str(PLANNED["view_orientation"])
|
|
548
|
+
if not isinstance(payload, dict):
|
|
549
|
+
raise ValueError("export_image must be an object payload")
|
|
550
|
+
require_result(await adapter.export_image(payload), "export_image")
|
|
551
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": "Exported image"}})
|
|
552
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after export_image")
|
|
553
|
+
|
|
554
|
+
elif tool == "check_interference":
|
|
555
|
+
payload = require_key("check_interference")
|
|
556
|
+
if not isinstance(payload, dict):
|
|
557
|
+
raise ValueError("check_interference must be an object payload")
|
|
558
|
+
require_result(await adapter.check_interference(payload), "check_interference")
|
|
559
|
+
tool_runs.append({{"tool": tool, "status": "success", "message": "Checked interference"}})
|
|
560
|
+
pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after check_interference")
|
|
561
|
+
|
|
562
|
+
else:
|
|
563
|
+
raise ValueError(f"Unsupported tool '{{tool}}' in strict script mode")
|
|
564
|
+
|
|
565
|
+
except Exception as step_exc:
|
|
566
|
+
failed_tools.append(tool)
|
|
567
|
+
tool_runs.append({{"tool": tool, "status": "error", "message": str(step_exc)}})
|
|
568
|
+
break
|
|
569
|
+
|
|
570
|
+
finally:
|
|
571
|
+
try:
|
|
572
|
+
await adapter.disconnect()
|
|
573
|
+
except Exception:
|
|
574
|
+
pass
|
|
575
|
+
|
|
576
|
+
return {{
|
|
577
|
+
"tool_runs": tool_runs,
|
|
578
|
+
"failed_tools": failed_tools,
|
|
579
|
+
}}
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def main() -> int:
|
|
583
|
+
try:
|
|
584
|
+
summary = asyncio.run(run_checkpoint())
|
|
585
|
+
print("CHECKPOINT_SCRIPT_RESULT::" + json.dumps(summary, ensure_ascii=True))
|
|
586
|
+
return 0 if not summary.get("failed_tools") else 1
|
|
587
|
+
except Exception as exc:
|
|
588
|
+
summary = {{
|
|
589
|
+
"tool_runs": [
|
|
590
|
+
{{"tool": "checkpoint.script", "status": "error", "message": str(exc)}},
|
|
591
|
+
{{"tool": "checkpoint.script", "status": "error", "message": traceback.format_exc()}},
|
|
592
|
+
],
|
|
593
|
+
"failed_tools": ["checkpoint.script"],
|
|
594
|
+
}}
|
|
595
|
+
print("CHECKPOINT_SCRIPT_RESULT::" + json.dumps(summary, ensure_ascii=True))
|
|
596
|
+
return 1
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
if __name__ == "__main__":
|
|
600
|
+
raise SystemExit(main())
|
|
601
|
+
"""
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
# ---------------------------------------------------------------------------
|
|
606
|
+
# Direct-execution helpers
|
|
607
|
+
# ---------------------------------------------------------------------------
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _pf(planned: dict[str, Any], *keys: str, default: float = 0.0) -> float:
|
|
611
|
+
"""Return the first float-convertible value found under any of *keys*."""
|
|
612
|
+
for key in keys:
|
|
613
|
+
if key in planned:
|
|
614
|
+
try:
|
|
615
|
+
return float(planned[key])
|
|
616
|
+
except (TypeError, ValueError):
|
|
617
|
+
pass
|
|
618
|
+
return default
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _pv(
|
|
622
|
+
planned: dict[str, Any],
|
|
623
|
+
*keys: str,
|
|
624
|
+
size: int = 4,
|
|
625
|
+
default: list[float] | None = None,
|
|
626
|
+
) -> list[float]:
|
|
627
|
+
"""Return a float list of *size* from the first matching key in planned."""
|
|
628
|
+
for key in keys:
|
|
629
|
+
val = planned.get(key)
|
|
630
|
+
if isinstance(val, list) and len(val) == size:
|
|
631
|
+
try:
|
|
632
|
+
return [float(v) for v in val]
|
|
633
|
+
except (TypeError, ValueError):
|
|
634
|
+
pass
|
|
635
|
+
return default if default is not None else [0.0] * size
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
async def _execute_tool(
|
|
639
|
+
adapter: Any,
|
|
640
|
+
planned: dict[str, Any],
|
|
641
|
+
tool: str,
|
|
642
|
+
) -> dict[str, Any] | None:
|
|
643
|
+
"""Dispatch *tool* to the adapter using parameters from *planned*.
|
|
644
|
+
|
|
645
|
+
Returns a tool_run dict on success, raises on failure, or returns None for
|
|
646
|
+
unknown tools (caller treats None as mocked).
|
|
647
|
+
Uses sensible defaults for any missing geometry parameters.
|
|
648
|
+
"""
|
|
649
|
+
from ...adapters.base import ExtrusionParameters # noqa: PLC0415
|
|
650
|
+
|
|
651
|
+
tool_payloads = _planned_tool_payloads(planned, tool)
|
|
652
|
+
ctx = dict(planned)
|
|
653
|
+
|
|
654
|
+
def _ok(result: Any, label: str) -> None:
|
|
655
|
+
if not getattr(result, "is_success", True):
|
|
656
|
+
raise RuntimeError(getattr(result, "error", f"{label} failed"))
|
|
657
|
+
|
|
658
|
+
if tool == "create_part":
|
|
659
|
+
name = str(ctx.get("part_name", "untitled_part"))
|
|
660
|
+
_ok(await adapter.create_part(name=name), "create_part")
|
|
661
|
+
return {"tool": tool, "status": "success", "message": f"Created part '{name}'"}
|
|
662
|
+
|
|
663
|
+
if tool == "create_assembly":
|
|
664
|
+
name = str(ctx.get("assembly_name", "untitled_assembly"))
|
|
665
|
+
_ok(await adapter.create_assembly(name=name), "create_assembly")
|
|
666
|
+
return {
|
|
667
|
+
"tool": tool,
|
|
668
|
+
"status": "success",
|
|
669
|
+
"message": f"Created assembly '{name}'",
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if tool == "open_model":
|
|
673
|
+
path = str(ctx.get("model_path", ""))
|
|
674
|
+
if not path:
|
|
675
|
+
raise ValueError("open_model requires model_path")
|
|
676
|
+
_ok(await adapter.open_model(path), "open_model")
|
|
677
|
+
return {"tool": tool, "status": "success", "message": f"Opened '{path}'"}
|
|
678
|
+
|
|
679
|
+
if tool == "create_sketch":
|
|
680
|
+
payloads = tool_payloads or [{}]
|
|
681
|
+
plane = "Front"
|
|
682
|
+
for payload in payloads:
|
|
683
|
+
plane = str({**ctx, **payload}.get("sketch_plane", "Front"))
|
|
684
|
+
_ok(await adapter.create_sketch(plane), "create_sketch")
|
|
685
|
+
return {
|
|
686
|
+
"tool": tool,
|
|
687
|
+
"status": "success",
|
|
688
|
+
"message": f"Created sketch on '{plane}'",
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if tool == "exit_sketch":
|
|
692
|
+
_ok(await adapter.exit_sketch(), "exit_sketch")
|
|
693
|
+
return {"tool": tool, "status": "success", "message": "Exited sketch"}
|
|
694
|
+
|
|
695
|
+
if tool == "add_line":
|
|
696
|
+
payloads = tool_payloads or [{}]
|
|
697
|
+
for payload in payloads:
|
|
698
|
+
merged = {**ctx, **payload}
|
|
699
|
+
if "line_mm" in merged:
|
|
700
|
+
x1, y1, x2, y2 = _pv(merged, "line_mm", size=4, default=[0, 0, 10, 10])
|
|
701
|
+
elif "line_start_mm" in merged:
|
|
702
|
+
sx, sy = _pv(merged, "line_start_mm", size=2, default=[0, 0])
|
|
703
|
+
ex, ey = _pv(merged, "line_end_mm", size=2, default=[10, 10])
|
|
704
|
+
x1, y1, x2, y2 = sx, sy, ex, ey
|
|
705
|
+
else:
|
|
706
|
+
x1, y1, x2, y2 = 0.0, 0.0, 10.0, 10.0
|
|
707
|
+
_ok(await adapter.add_line(x1, y1, x2, y2), "add_line")
|
|
708
|
+
return {"tool": tool, "status": "success", "message": "Added line"}
|
|
709
|
+
|
|
710
|
+
if tool == "add_rectangle":
|
|
711
|
+
payloads = tool_payloads or [{}]
|
|
712
|
+
for payload in payloads:
|
|
713
|
+
merged = {**ctx, **payload}
|
|
714
|
+
x, y, w, h = _pv(merged, "rectangle_mm", size=4, default=[0, 0, 50, 30])
|
|
715
|
+
_ok(await adapter.add_rectangle(x, y, w, h), "add_rectangle")
|
|
716
|
+
return {"tool": tool, "status": "success", "message": "Added rectangle"}
|
|
717
|
+
|
|
718
|
+
if tool == "add_circle":
|
|
719
|
+
payloads = tool_payloads or [{}]
|
|
720
|
+
for payload in payloads:
|
|
721
|
+
merged = {**ctx, **payload}
|
|
722
|
+
cx, cy = _pv(merged, "circle_center_mm", size=2, default=[0, 0])
|
|
723
|
+
radius = _pf(merged, "circle_radius_mm", default=5.0)
|
|
724
|
+
_ok(await adapter.add_circle(cx, cy, radius), "add_circle")
|
|
725
|
+
return {"tool": tool, "status": "success", "message": "Added circle"}
|
|
726
|
+
|
|
727
|
+
if tool == "add_centerline":
|
|
728
|
+
payloads = tool_payloads or [{}]
|
|
729
|
+
for payload in payloads:
|
|
730
|
+
merged = {**ctx, **payload}
|
|
731
|
+
x1, y1, x2, y2 = _pv(merged, "centerline_mm", size=4, default=[0, 0, 0, 50])
|
|
732
|
+
_ok(await adapter.add_centerline(x1, y1, x2, y2), "add_centerline")
|
|
733
|
+
return {"tool": tool, "status": "success", "message": "Added centerline"}
|
|
734
|
+
|
|
735
|
+
if tool == "add_arc":
|
|
736
|
+
payloads = tool_payloads or [{}]
|
|
737
|
+
for payload in payloads:
|
|
738
|
+
merged = {**ctx, **payload}
|
|
739
|
+
cx, cy = _pv(merged, "arc_center_mm", size=2, default=[0, 0])
|
|
740
|
+
sx, sy = _pv(merged, "arc_start_mm", size=2, default=[10, 0])
|
|
741
|
+
ex, ey = _pv(merged, "arc_end_mm", size=2, default=[0, 10])
|
|
742
|
+
_ok(await adapter.add_arc(cx, cy, sx, sy, ex, ey), "add_arc")
|
|
743
|
+
return {"tool": tool, "status": "success", "message": "Added arc"}
|
|
744
|
+
|
|
745
|
+
if tool == "create_extrusion":
|
|
746
|
+
payload = (tool_payloads or [{}])[0]
|
|
747
|
+
merged = {**ctx, **payload}
|
|
748
|
+
depth = _pf(merged, "depth_mm", "depth", default=10.0)
|
|
749
|
+
params = ExtrusionParameters(
|
|
750
|
+
depth=depth,
|
|
751
|
+
thin_feature=bool(merged.get("thin_feature", False)),
|
|
752
|
+
thin_thickness=float(
|
|
753
|
+
merged.get("thin_thickness_mm", merged.get("thin_thickness", 0.0))
|
|
754
|
+
),
|
|
755
|
+
both_directions=bool(merged.get("both_directions", False)),
|
|
756
|
+
)
|
|
757
|
+
_ok(await adapter.create_extrusion(params), "create_extrusion")
|
|
758
|
+
return {
|
|
759
|
+
"tool": tool,
|
|
760
|
+
"status": "success",
|
|
761
|
+
"message": f"Extrusion depth={depth}mm",
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if tool == "create_cut_extrude":
|
|
765
|
+
payload = (tool_payloads or [{}])[0]
|
|
766
|
+
merged = {**ctx, **payload}
|
|
767
|
+
depth = _pf(merged, "depth_mm", "depth", default=10.0)
|
|
768
|
+
_ok(
|
|
769
|
+
await adapter.create_cut_extrude(ExtrusionParameters(depth=depth)),
|
|
770
|
+
"create_cut_extrude",
|
|
771
|
+
)
|
|
772
|
+
return {
|
|
773
|
+
"tool": tool,
|
|
774
|
+
"status": "success",
|
|
775
|
+
"message": f"Cut-extrude depth={depth}mm",
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if tool == "create_cut":
|
|
779
|
+
payload = (tool_payloads or [{}])[0]
|
|
780
|
+
merged = {**ctx, **payload}
|
|
781
|
+
depth = _pf(merged, "depth_mm", "depth", default=10.0)
|
|
782
|
+
sketch_name = str(merged.get("sketch_name", ""))
|
|
783
|
+
if sketch_name:
|
|
784
|
+
_ok(await adapter.create_cut(sketch_name, depth), "create_cut")
|
|
785
|
+
return {
|
|
786
|
+
"tool": tool,
|
|
787
|
+
"status": "success",
|
|
788
|
+
"message": f"Cut from {sketch_name} depth={depth}mm",
|
|
789
|
+
}
|
|
790
|
+
_ok(
|
|
791
|
+
await adapter.create_cut_extrude(ExtrusionParameters(depth=depth)),
|
|
792
|
+
"create_cut",
|
|
793
|
+
)
|
|
794
|
+
return {
|
|
795
|
+
"tool": tool,
|
|
796
|
+
"status": "success",
|
|
797
|
+
"message": f"Cut-extrude depth={depth}mm",
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if tool == "add_fillet":
|
|
801
|
+
payload = (tool_payloads or [{}])[0]
|
|
802
|
+
merged = {**ctx, **payload}
|
|
803
|
+
radius = _pf(merged, "radius_mm", "radius", default=1.0)
|
|
804
|
+
raw = merged.get("edge_names", [])
|
|
805
|
+
edge_names = [str(e) for e in raw] if isinstance(raw, list) else []
|
|
806
|
+
_ok(await adapter.add_fillet(radius, edge_names), "add_fillet")
|
|
807
|
+
return {
|
|
808
|
+
"tool": tool,
|
|
809
|
+
"status": "success",
|
|
810
|
+
"message": f"Fillet radius={radius}mm",
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if tool == "check_sketch_fully_defined":
|
|
814
|
+
payload = (tool_payloads or [{}])[0]
|
|
815
|
+
check_sketch_name: str | None = {**ctx, **payload}.get("sketch_name")
|
|
816
|
+
_ok(
|
|
817
|
+
await adapter.check_sketch_fully_defined(
|
|
818
|
+
str(check_sketch_name) if check_sketch_name else None
|
|
819
|
+
),
|
|
820
|
+
"check_sketch_fully_defined",
|
|
821
|
+
)
|
|
822
|
+
return {
|
|
823
|
+
"tool": tool,
|
|
824
|
+
"status": "success",
|
|
825
|
+
"message": "Checked sketch definition",
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if tool == "save_file":
|
|
829
|
+
payload = (tool_payloads or [{}])[0]
|
|
830
|
+
file_path = str({**ctx, **payload}.get("file_path", ""))
|
|
831
|
+
if not file_path:
|
|
832
|
+
raise ValueError("save_file requires file_path")
|
|
833
|
+
_ok(await adapter.save_file(file_path), "save_file")
|
|
834
|
+
return {"tool": tool, "status": "success", "message": f"Saved '{file_path}'"}
|
|
835
|
+
|
|
836
|
+
if tool == "get_model_info":
|
|
837
|
+
_ok(await adapter.get_model_info(), "get_model_info")
|
|
838
|
+
return {"tool": tool, "status": "success", "message": "Retrieved model info"}
|
|
839
|
+
|
|
840
|
+
if tool == "list_features":
|
|
841
|
+
_ok(await adapter.list_features(include_suppressed=True), "list_features")
|
|
842
|
+
return {"tool": tool, "status": "success", "message": "Listed features"}
|
|
843
|
+
|
|
844
|
+
if tool == "get_mass_properties":
|
|
845
|
+
_ok(await adapter.get_mass_properties(), "get_mass_properties")
|
|
846
|
+
return {
|
|
847
|
+
"tool": tool,
|
|
848
|
+
"status": "success",
|
|
849
|
+
"message": "Retrieved mass properties",
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if tool == "analyze_geometry":
|
|
853
|
+
_ok(await adapter.get_model_info(), "get_model_info")
|
|
854
|
+
_ok(await adapter.list_features(include_suppressed=True), "list_features")
|
|
855
|
+
_ok(await adapter.get_mass_properties(), "get_mass_properties")
|
|
856
|
+
return {
|
|
857
|
+
"tool": tool,
|
|
858
|
+
"status": "success",
|
|
859
|
+
"message": "Geometry analysis completed",
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if tool == "export_image":
|
|
863
|
+
payloads = tool_payloads
|
|
864
|
+
payload = payloads[0] if payloads else ctx.get("export_image") or {}
|
|
865
|
+
if not isinstance(payload, dict):
|
|
866
|
+
raise ValueError("export_image must be an object payload")
|
|
867
|
+
_ok(await adapter.export_image(payload), "export_image")
|
|
868
|
+
return {"tool": tool, "status": "success", "message": "Exported image"}
|
|
869
|
+
|
|
870
|
+
if tool == "check_interference":
|
|
871
|
+
payloads = tool_payloads
|
|
872
|
+
payload = payloads[0] if payloads else ctx.get("check_interference") or {}
|
|
873
|
+
if not isinstance(payload, dict):
|
|
874
|
+
raise ValueError("check_interference must be an object payload")
|
|
875
|
+
_ok(await adapter.check_interference(payload), "check_interference")
|
|
876
|
+
return {
|
|
877
|
+
"tool": tool,
|
|
878
|
+
"status": "success",
|
|
879
|
+
"message": "Checked assembly interference",
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return None # Unknown tool → caller mocks it
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
async def _run_checkpoint_tools(
|
|
886
|
+
planned: dict[str, Any],
|
|
887
|
+
*,
|
|
888
|
+
session_id: str = DEFAULT_SESSION_ID,
|
|
889
|
+
checkpoint_index: int = 0,
|
|
890
|
+
checkpoint_title: str = "",
|
|
891
|
+
repair_context: dict[str, Any] | None = None,
|
|
892
|
+
) -> dict[str, Any]:
|
|
893
|
+
"""Execute checkpoint tools directly via the adapter with mocked-tool handling.
|
|
894
|
+
|
|
895
|
+
Generates the checkpoint script as an audit artifact but executes tools
|
|
896
|
+
in-process so that test adapters (monkeypatched create_adapter) work.
|
|
897
|
+
Unrecognized tool names are recorded as mocked
|
|
898
|
+
and do not block checkpoint success.
|
|
899
|
+
"""
|
|
900
|
+
tools = _planned_tools(planned)
|
|
901
|
+
script_dir = _checkpoint_script_dir(session_id)
|
|
902
|
+
script_dir.mkdir(parents=True, exist_ok=True)
|
|
903
|
+
script_path = _checkpoint_script_path(session_id, checkpoint_index)
|
|
904
|
+
script_text = _render_checkpoint_script(
|
|
905
|
+
planned,
|
|
906
|
+
session_id=session_id,
|
|
907
|
+
checkpoint_index=checkpoint_index,
|
|
908
|
+
checkpoint_title=checkpoint_title,
|
|
909
|
+
)
|
|
910
|
+
script_path.write_text(script_text, encoding="utf-8")
|
|
911
|
+
|
|
912
|
+
tool_runs: list[dict[str, Any]] = []
|
|
913
|
+
failed_tools: list[str] = []
|
|
914
|
+
mocked_tools: list[str] = []
|
|
915
|
+
|
|
916
|
+
config = load_config()
|
|
917
|
+
adapter = await create_adapter(config)
|
|
918
|
+
try:
|
|
919
|
+
await adapter.connect()
|
|
920
|
+
except Exception as adapter_error:
|
|
921
|
+
return {
|
|
922
|
+
"tool_runs": [
|
|
923
|
+
{
|
|
924
|
+
"tool": "checkpoint.execute",
|
|
925
|
+
"status": "error",
|
|
926
|
+
"message": f"Adapter connection failed: {adapter_error}",
|
|
927
|
+
}
|
|
928
|
+
],
|
|
929
|
+
"failed_tools": ["checkpoint.execute"],
|
|
930
|
+
"mocked_tools": [],
|
|
931
|
+
"script_path": str(script_path),
|
|
932
|
+
"script_text": script_text,
|
|
933
|
+
"stdout_text": "",
|
|
934
|
+
"stderr_text": str(adapter_error),
|
|
935
|
+
"validation_failures": [],
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
try:
|
|
939
|
+
for tool in tools:
|
|
940
|
+
try:
|
|
941
|
+
run = await _execute_tool(adapter, planned, tool)
|
|
942
|
+
except Exception as exc:
|
|
943
|
+
failed_tools.append(tool)
|
|
944
|
+
tool_runs.append({"tool": tool, "status": "error", "message": str(exc)})
|
|
945
|
+
break
|
|
946
|
+
|
|
947
|
+
if run is None:
|
|
948
|
+
mocked_tools.append(tool)
|
|
949
|
+
tool_runs.append(
|
|
950
|
+
{
|
|
951
|
+
"tool": tool,
|
|
952
|
+
"status": "mocked",
|
|
953
|
+
"message": f"Unknown tool '{tool}' — mocked",
|
|
954
|
+
}
|
|
955
|
+
)
|
|
956
|
+
else:
|
|
957
|
+
tool_runs.append(run)
|
|
958
|
+
finally:
|
|
959
|
+
try:
|
|
960
|
+
await adapter.disconnect()
|
|
961
|
+
except Exception:
|
|
962
|
+
pass
|
|
963
|
+
|
|
964
|
+
return {
|
|
965
|
+
"tool_runs": tool_runs,
|
|
966
|
+
"failed_tools": failed_tools,
|
|
967
|
+
"mocked_tools": mocked_tools,
|
|
968
|
+
"script_path": str(script_path),
|
|
969
|
+
"script_text": script_text,
|
|
970
|
+
"stdout_text": "",
|
|
971
|
+
"stderr_text": "",
|
|
972
|
+
"validation_failures": [],
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
# ---------------------------------------------------------------------------
|
|
977
|
+
# Public API
|
|
978
|
+
# ---------------------------------------------------------------------------
|
|
979
|
+
async def execute_next_checkpoint(
|
|
980
|
+
session_id: str = DEFAULT_SESSION_ID,
|
|
981
|
+
*,
|
|
982
|
+
db_path: Path | None = None,
|
|
983
|
+
) -> dict[str, Any]:
|
|
984
|
+
"""Find and execute the next un-executed checkpoint in the session plan.
|
|
985
|
+
|
|
986
|
+
Args:
|
|
987
|
+
session_id: Dashboard session identifier.
|
|
988
|
+
db_path: Optional SQLite path override.
|
|
989
|
+
|
|
990
|
+
Returns:
|
|
991
|
+
Full dashboard state payload.
|
|
992
|
+
"""
|
|
993
|
+
from .session_service import ( # noqa: PLC0415
|
|
994
|
+
build_dashboard_state,
|
|
995
|
+
ensure_dashboard_session,
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
session_row = ensure_dashboard_session(session_id, db_path=db_path)
|
|
999
|
+
checkpoints = list_plan_checkpoints(session_id, db_path=db_path)
|
|
1000
|
+
target = next((row for row in checkpoints if not row["executed"]), None)
|
|
1001
|
+
if target is None:
|
|
1002
|
+
merge_metadata(
|
|
1003
|
+
session_id,
|
|
1004
|
+
db_path=db_path,
|
|
1005
|
+
latest_message="All checkpoints have already been executed.",
|
|
1006
|
+
)
|
|
1007
|
+
return build_dashboard_state(session_id, db_path=db_path)
|
|
1008
|
+
|
|
1009
|
+
planned = parse_json_blob(target["planned_action_json"])
|
|
1010
|
+
session_meta = parse_json_blob(session_row.get("metadata_json"))
|
|
1011
|
+
|
|
1012
|
+
if _should_open_empty_part(session_meta, planned):
|
|
1013
|
+
await _open_empty_part_before_checkpoint(
|
|
1014
|
+
session_id=session_id,
|
|
1015
|
+
session_row=session_row,
|
|
1016
|
+
db_path=db_path,
|
|
1017
|
+
api_origin=DEFAULT_API_ORIGIN,
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
run_summary = await _run_checkpoint_tools(
|
|
1021
|
+
planned,
|
|
1022
|
+
session_id=session_id,
|
|
1023
|
+
checkpoint_index=int(target["checkpoint_index"]),
|
|
1024
|
+
checkpoint_title=str(target["title"]),
|
|
1025
|
+
repair_context={
|
|
1026
|
+
"active_sketch_plane": session_meta.get("active_sketch_plane"),
|
|
1027
|
+
"last_sketch_name": session_meta.get("last_sketch_name"),
|
|
1028
|
+
"user_goal": session_row.get("user_goal") or DEFAULT_USER_GOAL,
|
|
1029
|
+
},
|
|
1030
|
+
)
|
|
1031
|
+
failed_tools = run_summary["failed_tools"]
|
|
1032
|
+
tool_runs = run_summary["tool_runs"]
|
|
1033
|
+
mocked_tools = run_summary.get("mocked_tools", [])
|
|
1034
|
+
executed = not failed_tools
|
|
1035
|
+
|
|
1036
|
+
if failed_tools:
|
|
1037
|
+
message = (
|
|
1038
|
+
f"Checkpoint {target['checkpoint_index']} failed on tools: "
|
|
1039
|
+
f"{', '.join(failed_tools)}."
|
|
1040
|
+
)
|
|
1041
|
+
elif mocked_tools and not any(r.get("status") == "success" for r in tool_runs):
|
|
1042
|
+
message = (
|
|
1043
|
+
f"Checkpoint {target['checkpoint_index']} executed "
|
|
1044
|
+
f"(tools MOCKED: {', '.join(mocked_tools)})."
|
|
1045
|
+
)
|
|
1046
|
+
else:
|
|
1047
|
+
message = (
|
|
1048
|
+
f"Executed checkpoint {target['checkpoint_index']}: {target['title']}."
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
result_json = json.dumps(
|
|
1052
|
+
{
|
|
1053
|
+
"status": "success" if executed else "error",
|
|
1054
|
+
"message": message,
|
|
1055
|
+
"tools": _planned_tools(planned),
|
|
1056
|
+
"tool_runs": tool_runs,
|
|
1057
|
+
"failed_tools": failed_tools,
|
|
1058
|
+
"script_path": run_summary.get("script_path", ""),
|
|
1059
|
+
"script_text": run_summary.get("script_text", ""),
|
|
1060
|
+
"stdout_tail": str(run_summary.get("stdout_text", ""))[-4000:],
|
|
1061
|
+
"stderr_tail": str(run_summary.get("stderr_text", ""))[-4000:],
|
|
1062
|
+
"validation_failures": run_summary.get("validation_failures", []),
|
|
1063
|
+
},
|
|
1064
|
+
ensure_ascii=True,
|
|
1065
|
+
)
|
|
1066
|
+
update_plan_checkpoint(
|
|
1067
|
+
int(target["id"]),
|
|
1068
|
+
approved_by_user=True,
|
|
1069
|
+
executed=executed,
|
|
1070
|
+
result_json=result_json,
|
|
1071
|
+
db_path=db_path,
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
for tool_run in tool_runs:
|
|
1075
|
+
insert_tool_call_record(
|
|
1076
|
+
session_id=session_id,
|
|
1077
|
+
checkpoint_id=int(target["id"]),
|
|
1078
|
+
tool_name=tool_run["tool"],
|
|
1079
|
+
input_json=json.dumps(planned, ensure_ascii=True),
|
|
1080
|
+
output_json=json.dumps(tool_run, ensure_ascii=True),
|
|
1081
|
+
success=tool_run["status"] == "success",
|
|
1082
|
+
db_path=db_path,
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
upsert_design_session(
|
|
1086
|
+
session_id=session_id,
|
|
1087
|
+
user_goal=session_row.get("user_goal") or DEFAULT_USER_GOAL,
|
|
1088
|
+
source_mode=session_row.get("source_mode") or DEFAULT_SOURCE_MODE,
|
|
1089
|
+
accepted_family=session_row.get("accepted_family"),
|
|
1090
|
+
status="executing" if executed else "error",
|
|
1091
|
+
current_checkpoint_index=(
|
|
1092
|
+
target["checkpoint_index"]
|
|
1093
|
+
if executed
|
|
1094
|
+
else session_row.get("current_checkpoint_index") or 0
|
|
1095
|
+
),
|
|
1096
|
+
metadata_json=session_row.get("metadata_json"),
|
|
1097
|
+
db_path=db_path,
|
|
1098
|
+
)
|
|
1099
|
+
merge_metadata(
|
|
1100
|
+
session_id,
|
|
1101
|
+
db_path=db_path,
|
|
1102
|
+
latest_message=message,
|
|
1103
|
+
latest_error_text=(message if failed_tools else ""),
|
|
1104
|
+
remediation_hint=(
|
|
1105
|
+
"Open the generated checkpoint script, repair planned parameters, then rerun this checkpoint."
|
|
1106
|
+
if failed_tools
|
|
1107
|
+
else ""
|
|
1108
|
+
),
|
|
1109
|
+
)
|
|
1110
|
+
return build_dashboard_state(session_id, db_path=db_path)
|