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,650 @@
|
|
|
1
|
+
"""SolidWorks-as-Code exporter.
|
|
2
|
+
|
|
3
|
+
Reads ToolCallRecord rows from SQLite for a session and emits a clean,
|
|
4
|
+
runnable Python script that mirrors the structure of build_u_bracket_artifact.py.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from solidworks_mcp.agents.soc_exporter import export_session
|
|
9
|
+
|
|
10
|
+
export_session("my-session-id", output_path="my_part.py")
|
|
11
|
+
|
|
12
|
+
CLI::
|
|
13
|
+
|
|
14
|
+
python -m solidworks_mcp.agents.soc_exporter <session_id> <output_path>
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
import textwrap
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from .history_db import list_tool_call_records
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Helpers
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
_SCRIPT_HEADER = """\
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import asyncio
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
from solidworks_mcp.adapters import create_adapter
|
|
39
|
+
from solidworks_mcp.adapters.base import ExtrusionParameters
|
|
40
|
+
from solidworks_mcp.config import load_config
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def require(result: Any, label: str) -> Any:
|
|
44
|
+
if not result.is_success:
|
|
45
|
+
raise RuntimeError(f"{label} failed: {result.error}")
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def build_part() -> None:
|
|
50
|
+
config = load_config()
|
|
51
|
+
adapter = await create_adapter(config)
|
|
52
|
+
await adapter.connect()
|
|
53
|
+
try:
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
_SCRIPT_FOOTER = """\
|
|
57
|
+
finally:
|
|
58
|
+
await adapter.disconnect()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
asyncio.run(build_part())
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _r(v: Any) -> str:
|
|
67
|
+
"""Compact repr: omit None values, prefer bare strings for short strs."""
|
|
68
|
+
return repr(v)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _fmt_num(v: float) -> str:
|
|
72
|
+
"""Format a float without trailing zeros."""
|
|
73
|
+
s = f"{v:.6g}"
|
|
74
|
+
return s
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _parse_input(input_json: str | None) -> dict[str, Any]:
|
|
78
|
+
if not input_json:
|
|
79
|
+
return {}
|
|
80
|
+
try:
|
|
81
|
+
result = json.loads(input_json)
|
|
82
|
+
return result if isinstance(result, dict) else {}
|
|
83
|
+
except (json.JSONDecodeError, TypeError):
|
|
84
|
+
return {}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _parse_output(output_json: str | None) -> dict[str, Any]:
|
|
88
|
+
if not output_json:
|
|
89
|
+
return {}
|
|
90
|
+
try:
|
|
91
|
+
result = json.loads(output_json)
|
|
92
|
+
return result if isinstance(result, dict) else {}
|
|
93
|
+
except (json.JSONDecodeError, TypeError):
|
|
94
|
+
return {}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _coord(inp: dict[str, Any], *keys: str, default: float = 0.0) -> float:
|
|
98
|
+
"""Return the first non-None value from inp for the given keys as a float."""
|
|
99
|
+
for k in keys:
|
|
100
|
+
v = inp.get(k)
|
|
101
|
+
if v is not None:
|
|
102
|
+
return float(v)
|
|
103
|
+
return default
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _entity_id_from_output(output: dict[str, Any]) -> str | None:
|
|
107
|
+
"""Extract entity_id from an AdapterResult output dict."""
|
|
108
|
+
data = output.get("data")
|
|
109
|
+
if isinstance(data, dict):
|
|
110
|
+
return data.get("entity_id") or data.get("id")
|
|
111
|
+
if isinstance(data, str):
|
|
112
|
+
return data
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
# Per-tool code emitters
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class _CodeGen:
|
|
122
|
+
"""Stateful generator that tracks entity variables across one sketch."""
|
|
123
|
+
|
|
124
|
+
def __init__(self) -> None:
|
|
125
|
+
self._lines: list[str] = []
|
|
126
|
+
# entity_id (str like "Line_1") → python variable name (str like "line_1")
|
|
127
|
+
self._entity_vars: dict[str, str] = {}
|
|
128
|
+
self._counters: dict[str, int] = {}
|
|
129
|
+
self._in_sketch = False
|
|
130
|
+
|
|
131
|
+
def _indent(self, code: str) -> str:
|
|
132
|
+
return textwrap.indent(code, " ")
|
|
133
|
+
|
|
134
|
+
def _emit(self, line: str) -> None:
|
|
135
|
+
self._lines.append(self._indent(line))
|
|
136
|
+
|
|
137
|
+
def _blank(self) -> None:
|
|
138
|
+
self._lines.append("")
|
|
139
|
+
|
|
140
|
+
def _fresh_var(self, prefix: str) -> str:
|
|
141
|
+
n = self._counters.get(prefix, 0) + 1
|
|
142
|
+
self._counters[prefix] = n
|
|
143
|
+
return f"{prefix}_{n}"
|
|
144
|
+
|
|
145
|
+
def _reset_sketch_state(self) -> None:
|
|
146
|
+
self._entity_vars = {}
|
|
147
|
+
self._counters = {}
|
|
148
|
+
self._in_sketch = False
|
|
149
|
+
|
|
150
|
+
def _var_for(self, entity_id: str | None, prefix: str) -> str:
|
|
151
|
+
"""Return (and register) a variable name for entity_id."""
|
|
152
|
+
if entity_id and entity_id in self._entity_vars:
|
|
153
|
+
return self._entity_vars[entity_id]
|
|
154
|
+
var = self._fresh_var(prefix)
|
|
155
|
+
if entity_id:
|
|
156
|
+
self._entity_vars[entity_id] = var
|
|
157
|
+
return var
|
|
158
|
+
|
|
159
|
+
def _entity_ref(self, entity_id: str | None) -> str:
|
|
160
|
+
"""Return the python variable for a previously captured entity, or quoted id."""
|
|
161
|
+
if entity_id and entity_id in self._entity_vars:
|
|
162
|
+
return self._entity_vars[entity_id]
|
|
163
|
+
return _r(entity_id)
|
|
164
|
+
|
|
165
|
+
# ------------------------------------------------------------------
|
|
166
|
+
# Emitters per tool
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
def emit_create_part(self, inp: dict[str, Any], _out: dict[str, Any]) -> None:
|
|
170
|
+
name = inp.get("name", "part")
|
|
171
|
+
self._blank()
|
|
172
|
+
self._emit(
|
|
173
|
+
f'require(await adapter.create_part(name={_r(name)}), "create_part")'
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def emit_open_model(self, inp: dict[str, Any], _out: dict[str, Any]) -> None:
|
|
177
|
+
path = inp.get("file_path") or inp.get("path", "")
|
|
178
|
+
self._blank()
|
|
179
|
+
self._emit(f'require(await adapter.open_model({_r(path)}), "open_model")')
|
|
180
|
+
|
|
181
|
+
def emit_create_sketch(self, inp: dict[str, Any], _out: dict[str, Any]) -> None:
|
|
182
|
+
self._reset_sketch_state()
|
|
183
|
+
self._in_sketch = True
|
|
184
|
+
plane = inp.get("plane", "Front")
|
|
185
|
+
self._blank()
|
|
186
|
+
self._emit(
|
|
187
|
+
f'require(await adapter.create_sketch({_r(plane)}), "create_sketch")'
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def emit_exit_sketch(self, _inp: dict[str, Any], _out: dict[str, Any]) -> None:
|
|
191
|
+
self._in_sketch = False
|
|
192
|
+
self._blank()
|
|
193
|
+
self._emit('require(await adapter.exit_sketch(), "exit_sketch")')
|
|
194
|
+
|
|
195
|
+
def emit_add_line(self, inp: dict[str, Any], out: dict[str, Any]) -> None:
|
|
196
|
+
x1 = _coord(inp, "x1", "start_x")
|
|
197
|
+
y1 = _coord(inp, "y1", "start_y")
|
|
198
|
+
x2 = _coord(inp, "x2", "end_x")
|
|
199
|
+
y2 = _coord(inp, "y2", "end_y")
|
|
200
|
+
eid = _entity_id_from_output(out)
|
|
201
|
+
var = self._var_for(eid, "line")
|
|
202
|
+
args = f"{_fmt_num(x1)}, {_fmt_num(y1)}, {_fmt_num(x2)}, {_fmt_num(y2)}"
|
|
203
|
+
self._emit(f'{var} = require(await adapter.add_line({args}), "add_line")')
|
|
204
|
+
|
|
205
|
+
def emit_add_centerline(self, inp: dict[str, Any], out: dict[str, Any]) -> None:
|
|
206
|
+
x1 = _coord(inp, "x1", "start_x")
|
|
207
|
+
y1 = _coord(inp, "y1", "start_y")
|
|
208
|
+
x2 = _coord(inp, "x2", "end_x")
|
|
209
|
+
y2 = _coord(inp, "y2", "end_y")
|
|
210
|
+
eid = _entity_id_from_output(out)
|
|
211
|
+
var = self._var_for(eid, "cl")
|
|
212
|
+
args = f"{_fmt_num(x1)}, {_fmt_num(y1)}, {_fmt_num(x2)}, {_fmt_num(y2)}"
|
|
213
|
+
self._emit(
|
|
214
|
+
f'{var} = require(await adapter.add_centerline({args}), "add_centerline")'
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def emit_add_circle(self, inp: dict[str, Any], out: dict[str, Any]) -> None:
|
|
218
|
+
cx = inp.get("center_x", 0.0)
|
|
219
|
+
cy = inp.get("center_y", 0.0)
|
|
220
|
+
r = inp.get("radius", 1.0)
|
|
221
|
+
eid = _entity_id_from_output(out)
|
|
222
|
+
var = self._var_for(eid, "circle")
|
|
223
|
+
args = f"{_fmt_num(cx)}, {_fmt_num(cy)}, {_fmt_num(r)}"
|
|
224
|
+
self._emit(f'{var} = require(await adapter.add_circle({args}), "add_circle")')
|
|
225
|
+
|
|
226
|
+
def emit_add_arc(self, inp: dict[str, Any], out: dict[str, Any]) -> None:
|
|
227
|
+
cx = inp.get("center_x", 0.0)
|
|
228
|
+
cy = inp.get("center_y", 0.0)
|
|
229
|
+
r = inp.get("radius", 1.0)
|
|
230
|
+
start_angle = inp.get("start_angle", 0.0)
|
|
231
|
+
end_angle = inp.get("end_angle", 90.0)
|
|
232
|
+
eid = _entity_id_from_output(out)
|
|
233
|
+
var = self._var_for(eid, "arc")
|
|
234
|
+
args = (
|
|
235
|
+
f"{_fmt_num(cx)}, {_fmt_num(cy)}, {_fmt_num(r)}, "
|
|
236
|
+
f"{_fmt_num(start_angle)}, {_fmt_num(end_angle)}"
|
|
237
|
+
)
|
|
238
|
+
self._emit(f'{var} = require(await adapter.add_arc({args}), "add_arc")')
|
|
239
|
+
|
|
240
|
+
def emit_add_rectangle(self, inp: dict[str, Any], out: dict[str, Any]) -> None:
|
|
241
|
+
x1 = inp.get("x1", 0.0)
|
|
242
|
+
y1 = inp.get("y1", 0.0)
|
|
243
|
+
x2 = inp.get("x2", 1.0)
|
|
244
|
+
y2 = inp.get("y2", 1.0)
|
|
245
|
+
eid = _entity_id_from_output(out)
|
|
246
|
+
var = self._var_for(eid, "rect")
|
|
247
|
+
args = f"{_fmt_num(x1)}, {_fmt_num(y1)}, {_fmt_num(x2)}, {_fmt_num(y2)}"
|
|
248
|
+
self._emit(
|
|
249
|
+
f'{var} = require(await adapter.add_rectangle({args}), "add_rectangle")'
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def emit_add_spline(self, inp: dict[str, Any], out: dict[str, Any]) -> None:
|
|
253
|
+
points = inp.get("points", [])
|
|
254
|
+
eid = _entity_id_from_output(out)
|
|
255
|
+
var = self._var_for(eid, "spline")
|
|
256
|
+
self._emit(
|
|
257
|
+
f'{var} = require(await adapter.add_spline({_r(points)}), "add_spline")'
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def emit_add_polygon(self, inp: dict[str, Any], out: dict[str, Any]) -> None:
|
|
261
|
+
cx = inp.get("center_x", 0.0)
|
|
262
|
+
cy = inp.get("center_y", 0.0)
|
|
263
|
+
r = inp.get("radius", 1.0)
|
|
264
|
+
sides = inp.get("sides", 6)
|
|
265
|
+
eid = _entity_id_from_output(out)
|
|
266
|
+
var = self._var_for(eid, "polygon")
|
|
267
|
+
args = f"{_fmt_num(cx)}, {_fmt_num(cy)}, {_fmt_num(r)}, {sides}"
|
|
268
|
+
self._emit(f'{var} = require(await adapter.add_polygon({args}), "add_polygon")')
|
|
269
|
+
|
|
270
|
+
def emit_add_ellipse(self, inp: dict[str, Any], out: dict[str, Any]) -> None:
|
|
271
|
+
cx = inp.get("center_x", 0.0)
|
|
272
|
+
cy = inp.get("center_y", 0.0)
|
|
273
|
+
major = inp.get("major_axis", 1.0)
|
|
274
|
+
minor = inp.get("minor_axis", 0.5)
|
|
275
|
+
eid = _entity_id_from_output(out)
|
|
276
|
+
var = self._var_for(eid, "ellipse")
|
|
277
|
+
args = f"{_fmt_num(cx)}, {_fmt_num(cy)}, {_fmt_num(major)}, {_fmt_num(minor)}"
|
|
278
|
+
self._emit(f'{var} = require(await adapter.add_ellipse({args}), "add_ellipse")')
|
|
279
|
+
|
|
280
|
+
def emit_add_sketch_constraint(
|
|
281
|
+
self, inp: dict[str, Any], _out: dict[str, Any]
|
|
282
|
+
) -> None:
|
|
283
|
+
e1 = inp.get("entity1", "")
|
|
284
|
+
e2 = inp.get("entity2")
|
|
285
|
+
rel = inp.get("relation_type", "")
|
|
286
|
+
e3 = inp.get("entity3")
|
|
287
|
+
ref1 = self._entity_ref(e1)
|
|
288
|
+
ref2 = self._entity_ref(e2) if e2 else "None"
|
|
289
|
+
parts = [ref1, ref2, _r(rel)]
|
|
290
|
+
if e3:
|
|
291
|
+
parts.append(self._entity_ref(e3))
|
|
292
|
+
self._emit(
|
|
293
|
+
f'require(await adapter.add_sketch_constraint({", ".join(parts)}), "constraint {rel}")'
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def emit_add_sketch_dimension(
|
|
297
|
+
self, inp: dict[str, Any], _out: dict[str, Any]
|
|
298
|
+
) -> None:
|
|
299
|
+
e1 = inp.get("entity1", "")
|
|
300
|
+
e2 = inp.get("entity2")
|
|
301
|
+
dim_type = inp.get("dimension_type", "linear")
|
|
302
|
+
value = inp.get("value", 0.0)
|
|
303
|
+
ref1 = self._entity_ref(e1)
|
|
304
|
+
ref2 = self._entity_ref(e2) if e2 else "None"
|
|
305
|
+
self._emit(
|
|
306
|
+
f"require(await adapter.add_sketch_dimension("
|
|
307
|
+
f"{ref1}, {ref2}, {_r(dim_type)}, {_fmt_num(value)}), "
|
|
308
|
+
f'"dimension {_fmt_num(value)}")'
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def emit_create_extrusion(self, inp: dict[str, Any], _out: dict[str, Any]) -> None:
|
|
312
|
+
depth = inp.get("depth", 10.0)
|
|
313
|
+
thin = inp.get("thin_feature", False)
|
|
314
|
+
thin_t = inp.get("thin_thickness")
|
|
315
|
+
both = inp.get("both_directions", False)
|
|
316
|
+
draft = inp.get("draft_angle", 0.0)
|
|
317
|
+
reverse = inp.get("reverse_direction", False)
|
|
318
|
+
end_cond = inp.get("end_condition", "Blind")
|
|
319
|
+
|
|
320
|
+
params: list[str] = [f"depth={_fmt_num(depth)}"]
|
|
321
|
+
if thin:
|
|
322
|
+
params.append("thin_feature=True")
|
|
323
|
+
if thin_t is not None:
|
|
324
|
+
params.append(f"thin_thickness={_fmt_num(thin_t)}")
|
|
325
|
+
if both:
|
|
326
|
+
params.append("both_directions=True")
|
|
327
|
+
if draft:
|
|
328
|
+
params.append(f"draft_angle={_fmt_num(draft)}")
|
|
329
|
+
if reverse:
|
|
330
|
+
params.append("reverse_direction=True")
|
|
331
|
+
if end_cond != "Blind":
|
|
332
|
+
params.append(f"end_condition={_r(end_cond)}")
|
|
333
|
+
|
|
334
|
+
param_lines = "".join(f" {p},\n" for p in params)
|
|
335
|
+
self._blank()
|
|
336
|
+
self._lines.append(
|
|
337
|
+
" require(\n"
|
|
338
|
+
" await adapter.create_extrusion(\n"
|
|
339
|
+
" ExtrusionParameters(\n"
|
|
340
|
+
+ param_lines
|
|
341
|
+
+ " )\n"
|
|
342
|
+
" ),\n"
|
|
343
|
+
' "create_extrusion",\n'
|
|
344
|
+
" )"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def emit_create_cut_extrude(
|
|
348
|
+
self, inp: dict[str, Any], _out: dict[str, Any]
|
|
349
|
+
) -> None:
|
|
350
|
+
depth = inp.get("depth", 10.0)
|
|
351
|
+
through_all = inp.get("through_all", False)
|
|
352
|
+
reverse = inp.get("reverse_direction", False)
|
|
353
|
+
|
|
354
|
+
params: list[str] = []
|
|
355
|
+
if through_all:
|
|
356
|
+
params.append("through_all=True")
|
|
357
|
+
else:
|
|
358
|
+
params.append(f"depth={_fmt_num(depth)}")
|
|
359
|
+
if reverse:
|
|
360
|
+
params.append("reverse_direction=True")
|
|
361
|
+
|
|
362
|
+
self._blank()
|
|
363
|
+
self._emit(
|
|
364
|
+
f'require(await adapter.create_cut_extrude({", ".join(params)}), "create_cut_extrude")'
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def emit_save_file(self, inp: dict[str, Any], _out: dict[str, Any]) -> None:
|
|
368
|
+
path = inp.get("file_path") or inp.get("path", "")
|
|
369
|
+
self._blank()
|
|
370
|
+
self._emit(f'require(await adapter.save_file({_r(path)}), "save_file")')
|
|
371
|
+
|
|
372
|
+
def emit_save_as(self, inp: dict[str, Any], _out: dict[str, Any]) -> None:
|
|
373
|
+
path = inp.get("file_path") or inp.get("path", "")
|
|
374
|
+
self._blank()
|
|
375
|
+
self._emit(f'require(await adapter.save_as({_r(path)}), "save_as")')
|
|
376
|
+
|
|
377
|
+
def emit_export_image(self, inp: dict[str, Any], _out: dict[str, Any]) -> None:
|
|
378
|
+
path = inp.get("file_path", "")
|
|
379
|
+
fmt = inp.get("format_type", "png")
|
|
380
|
+
width = inp.get("width", 1600)
|
|
381
|
+
height = inp.get("height", 1000)
|
|
382
|
+
view = inp.get("view_orientation", "isometric")
|
|
383
|
+
self._blank()
|
|
384
|
+
self._lines.append(
|
|
385
|
+
" require(\n"
|
|
386
|
+
" await adapter.export_image({\n"
|
|
387
|
+
f' "file_path": {_r(path)},\n'
|
|
388
|
+
f' "format_type": {_r(fmt)},\n'
|
|
389
|
+
f' "width": {width},\n'
|
|
390
|
+
f' "height": {height},\n'
|
|
391
|
+
f' "view_orientation": {_r(view)},\n'
|
|
392
|
+
" }),\n"
|
|
393
|
+
' "export_image",\n'
|
|
394
|
+
" )"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
def emit_generic(self, bare_name: str, inp: dict[str, Any]) -> None:
|
|
398
|
+
"""Emit adapter.<bare_name>(**inp) for any tool not in _DISPATCH.
|
|
399
|
+
|
|
400
|
+
This gives every logged tool call valid, runnable Python code even if
|
|
401
|
+
the exporter has no specialized emitter for it. The adapter method
|
|
402
|
+
name and its keyword-argument names come directly from the logged
|
|
403
|
+
input_json, so they match the actual adapter API.
|
|
404
|
+
"""
|
|
405
|
+
if not inp:
|
|
406
|
+
self._blank()
|
|
407
|
+
self._emit(f'require(await adapter.{bare_name}(), "{bare_name}")')
|
|
408
|
+
else:
|
|
409
|
+
kwargs = ", ".join(f"{k}={_r(v)}" for k, v in inp.items() if v is not None)
|
|
410
|
+
self._blank()
|
|
411
|
+
self._emit(f'require(await adapter.{bare_name}({kwargs}), "{bare_name}")')
|
|
412
|
+
|
|
413
|
+
def emit_unknown(self, tool_name: str, inp: dict[str, Any]) -> None:
|
|
414
|
+
self._blank()
|
|
415
|
+
self._emit(f"# TODO: {tool_name}({inp!r})")
|
|
416
|
+
|
|
417
|
+
# ------------------------------------------------------------------
|
|
418
|
+
# Dispatch
|
|
419
|
+
# ------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
# These are pure reads — querying state without mutating the model.
|
|
422
|
+
# They're logged for audit purposes but omitted from the replay script.
|
|
423
|
+
_READ_ONLY_OPS: frozenset[str] = frozenset(
|
|
424
|
+
{
|
|
425
|
+
"get_model_info",
|
|
426
|
+
"list_features",
|
|
427
|
+
"list_configurations",
|
|
428
|
+
"get_mass_properties",
|
|
429
|
+
"get_dimension",
|
|
430
|
+
"check_sketch_fully_defined",
|
|
431
|
+
"analyze_geometry",
|
|
432
|
+
"check_interference",
|
|
433
|
+
"calculate_mass_properties",
|
|
434
|
+
}
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
_DISPATCH: dict[str, str] = {
|
|
438
|
+
"create_part": "emit_create_part",
|
|
439
|
+
"open_model": "emit_open_model",
|
|
440
|
+
"create_sketch": "emit_create_sketch",
|
|
441
|
+
"exit_sketch": "emit_exit_sketch",
|
|
442
|
+
"add_line": "emit_add_line",
|
|
443
|
+
"add_centerline": "emit_add_centerline",
|
|
444
|
+
"add_circle": "emit_add_circle",
|
|
445
|
+
"add_arc": "emit_add_arc",
|
|
446
|
+
"add_rectangle": "emit_add_rectangle",
|
|
447
|
+
"add_spline": "emit_add_spline",
|
|
448
|
+
"add_polygon": "emit_add_polygon",
|
|
449
|
+
"add_ellipse": "emit_add_ellipse",
|
|
450
|
+
"add_sketch_constraint": "emit_add_sketch_constraint",
|
|
451
|
+
"add_sketch_dimension": "emit_add_sketch_dimension",
|
|
452
|
+
"create_extrusion": "emit_create_extrusion",
|
|
453
|
+
"create_cut_extrude": "emit_create_cut_extrude",
|
|
454
|
+
"save_file": "emit_save_file",
|
|
455
|
+
"save_part": "emit_save_file",
|
|
456
|
+
"save_as": "emit_save_as",
|
|
457
|
+
"export_image": "emit_export_image",
|
|
458
|
+
"export_png": "emit_export_image",
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
def process(self, tool_name: str, inp: dict[str, Any], out: dict[str, Any]) -> None:
|
|
462
|
+
# Strip any MCP namespace prefix (e.g. "mcp__solidworks-mcp__create_part")
|
|
463
|
+
bare = tool_name.split("__")[-1] if "__" in tool_name else tool_name
|
|
464
|
+
# Skip internal bookkeeping and UI service calls
|
|
465
|
+
if bare in ("soc_create_checkpoint",) or bare.startswith("ui."):
|
|
466
|
+
return
|
|
467
|
+
# Skip read-only queries — they don't affect model state
|
|
468
|
+
if bare in self._READ_ONLY_OPS:
|
|
469
|
+
return
|
|
470
|
+
method = self._DISPATCH.get(bare)
|
|
471
|
+
if method:
|
|
472
|
+
getattr(self, method)(inp, out)
|
|
473
|
+
else:
|
|
474
|
+
# Agnostic fallback: emit adapter.<bare_name>(**kwargs) for any
|
|
475
|
+
# logged tool not in _DISPATCH (covers add_fillet, create_revolve,
|
|
476
|
+
# set_dimension, export_stl, close_model, create_assembly, etc.)
|
|
477
|
+
self.emit_generic(bare, inp)
|
|
478
|
+
|
|
479
|
+
def body_lines(self) -> list[str]:
|
|
480
|
+
return self._lines
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# ---------------------------------------------------------------------------
|
|
484
|
+
# Public API
|
|
485
|
+
# ---------------------------------------------------------------------------
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
_CHECKPOINT_RULE = "-" * 52
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _checkpoint_comment(cp: dict[str, Any]) -> str:
|
|
492
|
+
"""Render a SoCCheckpoint as a parseable comment block."""
|
|
493
|
+
label = cp.get("label", "")
|
|
494
|
+
file_path = cp.get("file_path", "")
|
|
495
|
+
first_id = cp.get("first_record_id")
|
|
496
|
+
last_id = cp.get("last_record_id")
|
|
497
|
+
records_range = (
|
|
498
|
+
f"{first_id}-{last_id}" if first_id and last_id else str(last_id or "")
|
|
499
|
+
)
|
|
500
|
+
lines = [
|
|
501
|
+
f" # -- checkpoint {_CHECKPOINT_RULE}",
|
|
502
|
+
f" # label: {label}",
|
|
503
|
+
f" # file: {file_path}",
|
|
504
|
+
]
|
|
505
|
+
if records_range:
|
|
506
|
+
lines.append(f" # records: {records_range}")
|
|
507
|
+
lines.append(f" # {_CHECKPOINT_RULE}")
|
|
508
|
+
return "\n".join(lines)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def render_single(
|
|
512
|
+
tool_name: str,
|
|
513
|
+
input_json: str | None,
|
|
514
|
+
output_json: str | None,
|
|
515
|
+
) -> str:
|
|
516
|
+
"""Render one tool call's SolidWorks-as-Code Python, in isolation.
|
|
517
|
+
|
|
518
|
+
Runs a fresh ``_CodeGen`` over a single record and returns its emitted
|
|
519
|
+
line(s) with the script-body indent stripped, so the result is
|
|
520
|
+
copy-pasteable on its own. Read-only / bookkeeping calls that emit nothing
|
|
521
|
+
return ``""``.
|
|
522
|
+
|
|
523
|
+
Note: this is deliberately stateless — entity variables (``line_1``,
|
|
524
|
+
``sketch_1`` …) that ``generate_script`` numbers across a whole session
|
|
525
|
+
all start from ``_1`` here. For a faithful multi-call script use
|
|
526
|
+
``generate_script`` / ``export_session``; this is for reading one call.
|
|
527
|
+
"""
|
|
528
|
+
gen = _CodeGen()
|
|
529
|
+
gen.process(
|
|
530
|
+
tool_name,
|
|
531
|
+
_parse_input(input_json),
|
|
532
|
+
_parse_output(output_json),
|
|
533
|
+
)
|
|
534
|
+
body = "\n".join(gen.body_lines())
|
|
535
|
+
return textwrap.dedent(body).strip("\n")
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def generate_script(
|
|
539
|
+
records: list[dict[str, Any]],
|
|
540
|
+
*,
|
|
541
|
+
session_id: str | None = None,
|
|
542
|
+
checkpoints: list[dict[str, Any]] | None = None,
|
|
543
|
+
skip_failed: bool = True,
|
|
544
|
+
) -> str:
|
|
545
|
+
"""Generate a Python script from a list of ToolCallRecord dicts.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
records: Ordered list of ToolCallRecord dicts (from list_tool_call_records).
|
|
549
|
+
session_id: Optional session ID to embed in header comment.
|
|
550
|
+
checkpoints: Optional list of SoCCheckpoint dicts (from list_soc_checkpoints).
|
|
551
|
+
When provided, checkpoint comment blocks are inserted between the
|
|
552
|
+
corresponding record boundaries.
|
|
553
|
+
skip_failed: Skip records where success=False (default True).
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
Complete Python script as a string.
|
|
557
|
+
"""
|
|
558
|
+
# Build a mapping from last_record_id → checkpoint for fast lookup
|
|
559
|
+
cp_after: dict[int, dict[str, Any]] = {}
|
|
560
|
+
if checkpoints:
|
|
561
|
+
for cp in checkpoints:
|
|
562
|
+
lid = cp.get("last_record_id")
|
|
563
|
+
if lid is not None:
|
|
564
|
+
cp_after[int(lid)] = cp
|
|
565
|
+
|
|
566
|
+
gen = _CodeGen()
|
|
567
|
+
|
|
568
|
+
for rec in records:
|
|
569
|
+
if skip_failed and not rec.get("success", True):
|
|
570
|
+
continue
|
|
571
|
+
tool_name: str = rec.get("tool_name", "")
|
|
572
|
+
inp = _parse_input(rec.get("input_json"))
|
|
573
|
+
out = _parse_output(rec.get("output_json"))
|
|
574
|
+
gen.process(tool_name, inp, out)
|
|
575
|
+
|
|
576
|
+
# Emit checkpoint block if this record is the last in a checkpoint
|
|
577
|
+
rec_id = rec.get("id")
|
|
578
|
+
if rec_id is not None and int(rec_id) in cp_after:
|
|
579
|
+
gen._lines.append("")
|
|
580
|
+
gen._lines.append(_checkpoint_comment(cp_after[int(rec_id)]))
|
|
581
|
+
gen._lines.append("")
|
|
582
|
+
|
|
583
|
+
header_comment = ""
|
|
584
|
+
if session_id:
|
|
585
|
+
header_comment = f"# session_id: {session_id}\n"
|
|
586
|
+
|
|
587
|
+
body = "\n".join(gen.body_lines())
|
|
588
|
+
if not body.strip():
|
|
589
|
+
body = " pass # no recorded tool calls"
|
|
590
|
+
|
|
591
|
+
return f"{header_comment}{_SCRIPT_HEADER}{body}\n{_SCRIPT_FOOTER}"
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def export_session(
|
|
595
|
+
session_id: str,
|
|
596
|
+
output_path: str | Path,
|
|
597
|
+
*,
|
|
598
|
+
checkpoint_id: int | None = None,
|
|
599
|
+
db_path: Path | None = None,
|
|
600
|
+
skip_failed: bool = True,
|
|
601
|
+
) -> Path:
|
|
602
|
+
"""Export a session's ToolCallRecords as a runnable Python script.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
session_id: The session ID to export.
|
|
606
|
+
output_path: Destination .py file path.
|
|
607
|
+
checkpoint_id: If set, export only records from this checkpoint.
|
|
608
|
+
db_path: Override default SQLite DB path.
|
|
609
|
+
skip_failed: Skip failed tool call records (default True).
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
Path to the written file.
|
|
613
|
+
"""
|
|
614
|
+
from .history_db import list_soc_checkpoints
|
|
615
|
+
|
|
616
|
+
records = list_tool_call_records(
|
|
617
|
+
session_id, checkpoint_id=checkpoint_id, db_path=db_path
|
|
618
|
+
)
|
|
619
|
+
checkpoints = list_soc_checkpoints(session_id, db_path=db_path)
|
|
620
|
+
script = generate_script(
|
|
621
|
+
records,
|
|
622
|
+
session_id=session_id,
|
|
623
|
+
checkpoints=checkpoints,
|
|
624
|
+
skip_failed=skip_failed,
|
|
625
|
+
)
|
|
626
|
+
out = Path(output_path)
|
|
627
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
628
|
+
out.write_text(script, encoding="utf-8")
|
|
629
|
+
return out
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
# ---------------------------------------------------------------------------
|
|
633
|
+
# CLI
|
|
634
|
+
# ---------------------------------------------------------------------------
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _cli() -> None:
|
|
638
|
+
if len(sys.argv) < 3:
|
|
639
|
+
print(
|
|
640
|
+
"Usage: python -m solidworks_mcp.agents.soc_exporter <session_id> <output.py>"
|
|
641
|
+
)
|
|
642
|
+
sys.exit(1)
|
|
643
|
+
session_id = sys.argv[1]
|
|
644
|
+
output_path = Path(sys.argv[2])
|
|
645
|
+
written = export_session(session_id, output_path)
|
|
646
|
+
print(f"Exported {session_id!r} → {written}")
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
if __name__ == "__main__":
|
|
650
|
+
_cli()
|