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.
Files changed (115) hide show
  1. solidworks_mcp/__init__.py +57 -0
  2. solidworks_mcp/adapters/__init__.py +40 -0
  3. solidworks_mcp/adapters/base.py +1882 -0
  4. solidworks_mcp/adapters/circuit_breaker.py +2052 -0
  5. solidworks_mcp/adapters/com_executor.py +252 -0
  6. solidworks_mcp/adapters/complexity_analyzer.py +272 -0
  7. solidworks_mcp/adapters/connection_pool.py +1884 -0
  8. solidworks_mcp/adapters/factory.py +366 -0
  9. solidworks_mcp/adapters/intelligent_router.py +218 -0
  10. solidworks_mcp/adapters/mock_adapter.py +4016 -0
  11. solidworks_mcp/adapters/pywin32_adapter.py +2842 -0
  12. solidworks_mcp/adapters/solidworks/__init__.py +17 -0
  13. solidworks_mcp/adapters/solidworks/bitmap.py +180 -0
  14. solidworks_mcp/adapters/solidworks/com_utils.py +56 -0
  15. solidworks_mcp/adapters/solidworks/dimension_layout.py +753 -0
  16. solidworks_mcp/adapters/solidworks/documentation.py +1476 -0
  17. solidworks_mcp/adapters/solidworks/drawing_standard.py +1993 -0
  18. solidworks_mcp/adapters/solidworks/face_selection.py +392 -0
  19. solidworks_mcp/adapters/solidworks/features.py +3362 -0
  20. solidworks_mcp/adapters/solidworks/io.py +3649 -0
  21. solidworks_mcp/adapters/solidworks/rebuild_diagnostics.py +269 -0
  22. solidworks_mcp/adapters/solidworks/rebuild_inspector.py +146 -0
  23. solidworks_mcp/adapters/solidworks/selection.py +308 -0
  24. solidworks_mcp/adapters/solidworks/sketch.py +2545 -0
  25. solidworks_mcp/adapters/sw_feature_errors.py +66 -0
  26. solidworks_mcp/adapters/sw_type_info.py +317 -0
  27. solidworks_mcp/adapters/vba_adapter.py +318 -0
  28. solidworks_mcp/adapters/vba_macro_executor.py +215 -0
  29. solidworks_mcp/agents/__init__.py +68 -0
  30. solidworks_mcp/agents/design_knowledge/assembly_patterns.md +127 -0
  31. solidworks_mcp/agents/design_knowledge/hinges.md +118 -0
  32. solidworks_mcp/agents/design_knowledge/snap_fits.md +93 -0
  33. solidworks_mcp/agents/design_knowledge/solidworks_basics.md +3 -0
  34. solidworks_mcp/agents/design_knowledge/tolerancing.md +128 -0
  35. solidworks_mcp/agents/harness.py +216 -0
  36. solidworks_mcp/agents/history_db.py +1508 -0
  37. solidworks_mcp/agents/retrieval_index.py +164 -0
  38. solidworks_mcp/agents/schemas.py +165 -0
  39. solidworks_mcp/agents/smoke_test.py +285 -0
  40. solidworks_mcp/agents/soc_exporter.py +650 -0
  41. solidworks_mcp/agents/soc_pickup.py +332 -0
  42. solidworks_mcp/agents/soc_rewind.py +221 -0
  43. solidworks_mcp/agents/vector_rag.py +665 -0
  44. solidworks_mcp/cache/__init__.py +5 -0
  45. solidworks_mcp/cache/response_cache.py +164 -0
  46. solidworks_mcp/config.py +729 -0
  47. solidworks_mcp/exceptions.py +37 -0
  48. solidworks_mcp/security/__init__.py +68 -0
  49. solidworks_mcp/security/auth.py +128 -0
  50. solidworks_mcp/security/cors.py +27 -0
  51. solidworks_mcp/security/rate_limiting.py +90 -0
  52. solidworks_mcp/security/runtime.py +155 -0
  53. solidworks_mcp/server.py +777 -0
  54. solidworks_mcp/server_cli_fixed.py +120 -0
  55. solidworks_mcp/setup/__init__.py +1 -0
  56. solidworks_mcp/setup/cli.py +399 -0
  57. solidworks_mcp/setup/clients.py +157 -0
  58. solidworks_mcp/setup/config_file.py +230 -0
  59. solidworks_mcp/setup/handshake.py +202 -0
  60. solidworks_mcp/setup/launcher.py +102 -0
  61. solidworks_mcp/setup/legacy.py +100 -0
  62. solidworks_mcp/setup/registration.py +193 -0
  63. solidworks_mcp/setup/solidworks.py +49 -0
  64. solidworks_mcp/tools/__init__.py +64 -0
  65. solidworks_mcp/tools/analysis.py +723 -0
  66. solidworks_mcp/tools/automation.py +578 -0
  67. solidworks_mcp/tools/docs_discovery.py +1476 -0
  68. solidworks_mcp/tools/drawing.py +1015 -0
  69. solidworks_mcp/tools/drawing_analysis.py +456 -0
  70. solidworks_mcp/tools/drawing_automation.py +627 -0
  71. solidworks_mcp/tools/engineering_data.py +466 -0
  72. solidworks_mcp/tools/error_classifier.py +232 -0
  73. solidworks_mcp/tools/export.py +833 -0
  74. solidworks_mcp/tools/file_facts.py +40 -0
  75. solidworks_mcp/tools/file_management.py +1473 -0
  76. solidworks_mcp/tools/input_compat.py +17 -0
  77. solidworks_mcp/tools/macro_recording.py +464 -0
  78. solidworks_mcp/tools/modeling.py +2360 -0
  79. solidworks_mcp/tools/sketching.py +1422 -0
  80. solidworks_mcp/tools/template_management.py +367 -0
  81. solidworks_mcp/tools/tool_boundary.py +280 -0
  82. solidworks_mcp/tools/vba_generation.py +1145 -0
  83. solidworks_mcp/ui/__init__.py +9 -0
  84. solidworks_mcp/ui/local_llm.py +725 -0
  85. solidworks_mcp/ui/routers/__init__.py +10 -0
  86. solidworks_mcp/ui/routers/checkpoint.py +24 -0
  87. solidworks_mcp/ui/routers/docs.py +47 -0
  88. solidworks_mcp/ui/routers/llm.py +83 -0
  89. solidworks_mcp/ui/routers/local_model.py +69 -0
  90. solidworks_mcp/ui/routers/model.py +75 -0
  91. solidworks_mcp/ui/routers/preview.py +38 -0
  92. solidworks_mcp/ui/routers/session.py +161 -0
  93. solidworks_mcp/ui/routers/viewer.py +154 -0
  94. solidworks_mcp/ui/schemas.py +502 -0
  95. solidworks_mcp/ui/server.py +298 -0
  96. solidworks_mcp/ui/service.py +481 -0
  97. solidworks_mcp/ui/services/__init__.py +136 -0
  98. solidworks_mcp/ui/services/_utils.py +958 -0
  99. solidworks_mcp/ui/services/checkpoint_service.py +1110 -0
  100. solidworks_mcp/ui/services/docs_service.py +282 -0
  101. solidworks_mcp/ui/services/llm_service.py +956 -0
  102. solidworks_mcp/ui/services/model_service.py +459 -0
  103. solidworks_mcp/ui/services/parameter_repair_service.py +314 -0
  104. solidworks_mcp/ui/services/preview_service.py +489 -0
  105. solidworks_mcp/ui/services/session_service.py +1389 -0
  106. solidworks_mcp/utils/__init__.py +9 -0
  107. solidworks_mcp/utils/feature_tree_classifier.py +342 -0
  108. solidworks_mcp/utils/logging.py +76 -0
  109. solidworks_mcp/utils/validation.py +77 -0
  110. solidworks_mcp/version.py +8 -0
  111. solidworks_mcp_python-1.5.0.dist-info/METADATA +410 -0
  112. solidworks_mcp_python-1.5.0.dist-info/RECORD +115 -0
  113. solidworks_mcp_python-1.5.0.dist-info/WHEEL +4 -0
  114. solidworks_mcp_python-1.5.0.dist-info/entry_points.txt +3 -0
  115. 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()