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,332 @@
|
|
|
1
|
+
"""SolidWorks-as-Code pickup: reverse-engineer manual changes into the script.
|
|
2
|
+
|
|
3
|
+
When you make changes directly in SolidWorks after the last checkpoint (adding a
|
|
4
|
+
fillet, a hole, a sketch, etc.), this module detects them by diffing the current
|
|
5
|
+
feature tree against the last saved ModelStateSnapshot and emits new script lines
|
|
6
|
+
for the delta.
|
|
7
|
+
|
|
8
|
+
The pickup result can be appended to the existing generated script, and a new
|
|
9
|
+
checkpoint is created so subsequent pickups only diff against the new state.
|
|
10
|
+
|
|
11
|
+
Feature type coverage
|
|
12
|
+
─────────────────────
|
|
13
|
+
+--------------------------+--------------------------------------------------+
|
|
14
|
+
| Feature type keyword | Emitted code |
|
|
15
|
+
+--------------------------+--------------------------------------------------+
|
|
16
|
+
| Boss-Extrude / Extrude | create_extrusion(ExtrusionParameters(depth=?)) |
|
|
17
|
+
| Cut-Extrude / CutExtrude | create_cut_extrude(ExtrusionParameters(depth=?)) |
|
|
18
|
+
| Fillet / Round | add_fillet(radius=?, edge_names=[...]) |
|
|
19
|
+
| Chamfer | # TODO: add_chamfer(...) |
|
|
20
|
+
| Sketch | create_sketch(...); exit_sketch() |
|
|
21
|
+
| Reference plane | # TODO: add reference plane |
|
|
22
|
+
| Mirror / Pattern | # TODO: mirror / pattern feature |
|
|
23
|
+
| Everything else | # TODO: reconstruct <name> (<type>) |
|
|
24
|
+
+--------------------------+--------------------------------------------------+
|
|
25
|
+
|
|
26
|
+
Depth/radius values cannot be read without a live SolidWorks connection.
|
|
27
|
+
Placeholders are left for the user to fill in.
|
|
28
|
+
|
|
29
|
+
Usage::
|
|
30
|
+
|
|
31
|
+
from solidworks_mcp.agents.soc_pickup import pickup_changes
|
|
32
|
+
|
|
33
|
+
delta_lines = await pickup_changes(
|
|
34
|
+
adapter,
|
|
35
|
+
session_id="my-session",
|
|
36
|
+
checkpoint_label="post-pickup-1", # label for the new checkpoint
|
|
37
|
+
output_path="my_part.py", # append to existing script
|
|
38
|
+
)
|
|
39
|
+
print("\\n".join(delta_lines))
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import json
|
|
45
|
+
import sys
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
from typing import Any
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Feature diff
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _feature_map(feature_tree: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
|
55
|
+
return {f.get("name", ""): f for f in feature_tree if f.get("name")}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _feature_key(feature: dict[str, Any]) -> tuple[Any, str] | None:
|
|
59
|
+
"""Stable identity for a feature row: (owning component, name).
|
|
60
|
+
|
|
61
|
+
An assembly's flattened feature list commonly has the same name
|
|
62
|
+
(e.g. "Boss-Extrude1") in several different components, so bare name
|
|
63
|
+
alone isn't unique across an assembly. Pairing it with ``component``
|
|
64
|
+
(``None`` for a document's own features) disambiguates that while
|
|
65
|
+
leaving Part-only trees - where every row's ``component`` is ``None``
|
|
66
|
+
- behaving exactly as bare-name comparison did before.
|
|
67
|
+
"""
|
|
68
|
+
name = feature.get("name")
|
|
69
|
+
if not name:
|
|
70
|
+
return None
|
|
71
|
+
return (feature.get("component"), name)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def diff_feature_trees(
|
|
75
|
+
old_tree: list[dict[str, Any]],
|
|
76
|
+
new_tree: list[dict[str, Any]],
|
|
77
|
+
) -> list[dict[str, Any]]:
|
|
78
|
+
"""Return features present in new_tree but not in old_tree (by component+name).
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
old_tree: Feature list from the last ModelStateSnapshot.
|
|
82
|
+
new_tree: Current feature list from list_features().
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
List of new feature dicts, in the order they appear in new_tree.
|
|
86
|
+
"""
|
|
87
|
+
old_keys = {key for f in old_tree if (key := _feature_key(f)) is not None}
|
|
88
|
+
return [f for f in new_tree if _feature_key(f) not in old_keys]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Code emission for detected features
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _feature_type(feature: dict[str, Any]) -> str:
|
|
97
|
+
return (feature.get("type") or feature.get("feature_type") or "").lower()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
_EXTRUDE_KEYWORDS = ("boss-extrude", "extrusion", "boss_extrude", "boss extrude")
|
|
101
|
+
_CUT_KEYWORDS = ("cut-extrude", "cut_extrude", "cut extrude", "cutextrude")
|
|
102
|
+
_FILLET_KEYWORDS = ("fillet", "round")
|
|
103
|
+
_CHAMFER_KEYWORDS = ("chamfer",)
|
|
104
|
+
_SKETCH_KEYWORDS = ("sketch",)
|
|
105
|
+
_PLANE_KEYWORDS = ("plane", "refplane", "reference plane")
|
|
106
|
+
_MIRROR_KEYWORDS = ("mirror", "mirrorsolid")
|
|
107
|
+
_PATTERN_KEYWORDS = ("linearpattern", "circularpattern", "pattern")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _classify(ftype: str) -> str:
|
|
111
|
+
for kw in _EXTRUDE_KEYWORDS:
|
|
112
|
+
if kw in ftype:
|
|
113
|
+
return "extrude"
|
|
114
|
+
for kw in _CUT_KEYWORDS:
|
|
115
|
+
if kw in ftype:
|
|
116
|
+
return "cut"
|
|
117
|
+
for kw in _FILLET_KEYWORDS:
|
|
118
|
+
if kw in ftype:
|
|
119
|
+
return "fillet"
|
|
120
|
+
for kw in _CHAMFER_KEYWORDS:
|
|
121
|
+
if kw in ftype:
|
|
122
|
+
return "chamfer"
|
|
123
|
+
for kw in _SKETCH_KEYWORDS:
|
|
124
|
+
if kw in ftype:
|
|
125
|
+
return "sketch"
|
|
126
|
+
for kw in _PLANE_KEYWORDS:
|
|
127
|
+
if kw in ftype:
|
|
128
|
+
return "plane"
|
|
129
|
+
for kw in _MIRROR_KEYWORDS:
|
|
130
|
+
if kw in ftype:
|
|
131
|
+
return "mirror"
|
|
132
|
+
for kw in _PATTERN_KEYWORDS:
|
|
133
|
+
if kw in ftype:
|
|
134
|
+
return "pattern"
|
|
135
|
+
return "unknown"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def emit_feature_lines(feature: dict[str, Any]) -> list[str]:
|
|
139
|
+
"""Emit Python code lines for a single newly-detected feature.
|
|
140
|
+
|
|
141
|
+
Values that require querying the COM API are left as '?' placeholders.
|
|
142
|
+
"""
|
|
143
|
+
name = feature.get("name", "unknown")
|
|
144
|
+
ftype = _feature_type(feature)
|
|
145
|
+
kind = _classify(ftype)
|
|
146
|
+
|
|
147
|
+
lines: list[str] = []
|
|
148
|
+
lines.append(f" # [pickup] {name!r} ({ftype or 'unknown type'})")
|
|
149
|
+
|
|
150
|
+
if kind == "extrude":
|
|
151
|
+
lines += [
|
|
152
|
+
" require(",
|
|
153
|
+
" await adapter.create_extrusion(",
|
|
154
|
+
" ExtrusionParameters(",
|
|
155
|
+
" depth=?, # TODO: fill in depth (mm)",
|
|
156
|
+
" )",
|
|
157
|
+
" ),",
|
|
158
|
+
f' "create_extrusion {name}",',
|
|
159
|
+
" )",
|
|
160
|
+
]
|
|
161
|
+
elif kind == "cut":
|
|
162
|
+
lines += [
|
|
163
|
+
" require(",
|
|
164
|
+
" await adapter.create_cut_extrude(",
|
|
165
|
+
" ExtrusionParameters(",
|
|
166
|
+
" depth=?, # TODO: fill in depth (mm)",
|
|
167
|
+
" )",
|
|
168
|
+
" ),",
|
|
169
|
+
f' "create_cut_extrude {name}",',
|
|
170
|
+
" )",
|
|
171
|
+
]
|
|
172
|
+
elif kind == "fillet":
|
|
173
|
+
lines += [
|
|
174
|
+
" require(",
|
|
175
|
+
f" await adapter.add_fillet(radius=?, edge_names=[]), # TODO: radius + edges for {name}",
|
|
176
|
+
f' "add_fillet {name}",',
|
|
177
|
+
" )",
|
|
178
|
+
]
|
|
179
|
+
elif kind == "sketch":
|
|
180
|
+
lines += [
|
|
181
|
+
" require(await adapter.create_sketch('?'), 'create_sketch') # TODO: pick plane",
|
|
182
|
+
" # TODO: add sketch entities here",
|
|
183
|
+
" require(await adapter.exit_sketch(), 'exit_sketch')",
|
|
184
|
+
]
|
|
185
|
+
elif kind in ("chamfer", "plane", "mirror", "pattern"):
|
|
186
|
+
lines.append(
|
|
187
|
+
f" # TODO: reconstruct {name!r} ({kind}) — no automatic emitter yet"
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
lines.append(
|
|
191
|
+
f" # TODO: reconstruct {name!r} — unknown feature type {ftype!r}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
lines.append("")
|
|
195
|
+
return lines
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def generate_pickup_lines(new_features: list[dict[str, Any]]) -> list[str]:
|
|
199
|
+
"""Generate script lines for a list of new features.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
new_features: Features returned by diff_feature_trees().
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
List of Python source lines (without trailing newlines).
|
|
206
|
+
"""
|
|
207
|
+
if not new_features:
|
|
208
|
+
return [" # [pickup] no new features detected"]
|
|
209
|
+
|
|
210
|
+
header = [
|
|
211
|
+
" # ── pickup ──────────────────────────────────────────────",
|
|
212
|
+
f" # {len(new_features)} new feature(s) detected since last checkpoint",
|
|
213
|
+
" # Fill in '?' placeholders before running.",
|
|
214
|
+
" # ──────────────────────────────────────────────────────────",
|
|
215
|
+
"",
|
|
216
|
+
]
|
|
217
|
+
body: list[str] = []
|
|
218
|
+
for feat in new_features:
|
|
219
|
+
body.extend(emit_feature_lines(feat))
|
|
220
|
+
|
|
221
|
+
return header + body
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
# Main async entry point
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
async def pickup_changes(
|
|
230
|
+
adapter: Any,
|
|
231
|
+
session_id: str,
|
|
232
|
+
*,
|
|
233
|
+
checkpoint_label: str = "pickup",
|
|
234
|
+
output_path: str | Path | None = None,
|
|
235
|
+
db_path: Path | None = None,
|
|
236
|
+
) -> list[str]:
|
|
237
|
+
"""Diff current feature tree against last snapshot and emit delta lines.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
adapter: Connected SolidWorks adapter.
|
|
241
|
+
session_id: The SoC session ID.
|
|
242
|
+
checkpoint_label: Label for the new checkpoint created after pickup.
|
|
243
|
+
output_path: If set, append the pickup lines to this .py script file.
|
|
244
|
+
db_path: Override default SQLite DB path.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
List of generated Python source lines for the new features.
|
|
248
|
+
"""
|
|
249
|
+
from .history_db import (
|
|
250
|
+
insert_model_state_snapshot,
|
|
251
|
+
list_model_state_snapshots,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# 1. Get current feature tree
|
|
255
|
+
feat_result = await adapter.list_features()
|
|
256
|
+
if not feat_result.is_success:
|
|
257
|
+
raise RuntimeError(f"list_features failed: {feat_result.error}")
|
|
258
|
+
current_tree: list[dict[str, Any]] = feat_result.data or []
|
|
259
|
+
|
|
260
|
+
# 2. Get last snapshot for this session
|
|
261
|
+
snapshots = list_model_state_snapshots(session_id, db_path=db_path)
|
|
262
|
+
old_tree: list[dict[str, Any]] = []
|
|
263
|
+
if snapshots:
|
|
264
|
+
raw = snapshots[0].get("feature_tree_json")
|
|
265
|
+
if raw:
|
|
266
|
+
try:
|
|
267
|
+
old_tree = json.loads(raw)
|
|
268
|
+
except (json.JSONDecodeError, TypeError):
|
|
269
|
+
old_tree = []
|
|
270
|
+
|
|
271
|
+
# 3. Diff
|
|
272
|
+
new_features = diff_feature_trees(old_tree, current_tree)
|
|
273
|
+
|
|
274
|
+
# 4. Generate pickup lines
|
|
275
|
+
pickup_lines = generate_pickup_lines(new_features)
|
|
276
|
+
|
|
277
|
+
# 5. Append to script file if requested
|
|
278
|
+
if output_path is not None:
|
|
279
|
+
path = Path(output_path)
|
|
280
|
+
existing = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
281
|
+
# Insert before the finally block
|
|
282
|
+
insertion = "\n".join(pickup_lines) + "\n"
|
|
283
|
+
if " finally:" in existing:
|
|
284
|
+
existing = existing.replace(" finally:", insertion + " finally:", 1)
|
|
285
|
+
else:
|
|
286
|
+
existing = existing.rstrip("\n") + "\n" + insertion
|
|
287
|
+
path.write_text(existing, encoding="utf-8")
|
|
288
|
+
|
|
289
|
+
# 6. Save updated snapshot and create new checkpoint
|
|
290
|
+
model_info_result = await adapter.get_model_info()
|
|
291
|
+
model_path = ""
|
|
292
|
+
if model_info_result.is_success and isinstance(model_info_result.data, dict):
|
|
293
|
+
model_path = str(model_info_result.data.get("file_path") or "")
|
|
294
|
+
|
|
295
|
+
insert_model_state_snapshot(
|
|
296
|
+
session_id=session_id,
|
|
297
|
+
model_path=model_path or None,
|
|
298
|
+
feature_tree_json=json.dumps(current_tree, default=str),
|
|
299
|
+
db_path=db_path,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if hasattr(adapter, "soc_create_checkpoint"):
|
|
303
|
+
await adapter.soc_create_checkpoint(
|
|
304
|
+
checkpoint_label,
|
|
305
|
+
model_path or "",
|
|
306
|
+
feature_tree=current_tree,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return pickup_lines
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# ---------------------------------------------------------------------------
|
|
313
|
+
# CLI
|
|
314
|
+
# ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _cli() -> None:
|
|
318
|
+
if len(sys.argv) < 2:
|
|
319
|
+
print(
|
|
320
|
+
"Usage: python -m solidworks_mcp.agents.soc_pickup <session_id> "
|
|
321
|
+
"[checkpoint_label]"
|
|
322
|
+
)
|
|
323
|
+
sys.exit(1)
|
|
324
|
+
print(
|
|
325
|
+
"soc_pickup CLI requires a running SolidWorks adapter. "
|
|
326
|
+
"Call pickup_changes() from a script instead."
|
|
327
|
+
)
|
|
328
|
+
sys.exit(1)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
if __name__ == "__main__":
|
|
332
|
+
_cli()
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""SolidWorks-as-Code rewind: roll back to a named checkpoint.
|
|
2
|
+
|
|
3
|
+
Two complementary operations:
|
|
4
|
+
|
|
5
|
+
1. **Model rewind** — open the .sldprt saved at a checkpoint in SolidWorks.
|
|
6
|
+
2. **Script rewind** — truncate a generated script to the checkpoint boundary,
|
|
7
|
+
returning only the lines up to (and including) the checkpoint comment block.
|
|
8
|
+
|
|
9
|
+
The combination gives you a clean slate: the model is at checkpoint state and
|
|
10
|
+
the script reflects exactly what produced it. Continue building from there by
|
|
11
|
+
appending new tool calls.
|
|
12
|
+
|
|
13
|
+
Usage::
|
|
14
|
+
|
|
15
|
+
from solidworks_mcp.agents.soc_rewind import rewind_to_checkpoint, truncate_script_at
|
|
16
|
+
|
|
17
|
+
# Open the model and get the truncated script
|
|
18
|
+
script_so_far = await rewind_to_checkpoint(
|
|
19
|
+
adapter,
|
|
20
|
+
session_id="my-session",
|
|
21
|
+
label="base-extrude",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
CLI (model rewind only — prints truncated script to stdout)::
|
|
25
|
+
|
|
26
|
+
python -m solidworks_mcp.agents.soc_rewind <session_id> <label>
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import re
|
|
32
|
+
import sys
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Script-side rewind (no SolidWorks required)
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
# Matches the opening line of a checkpoint comment block:
|
|
41
|
+
# " # ── checkpoint ──..."
|
|
42
|
+
_CP_OPEN_RE = re.compile(r"^\s*#\s*--\s*checkpoint", re.IGNORECASE)
|
|
43
|
+
# Matches " # label: <value>"
|
|
44
|
+
_CP_LABEL_RE = re.compile(r"^\s*#\s*label:\s*(.+)$", re.IGNORECASE)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_script_checkpoints(script_text: str) -> list[dict[str, Any]]:
|
|
48
|
+
"""Parse all checkpoint comment blocks from a generated SoC script.
|
|
49
|
+
|
|
50
|
+
Returns a list of dicts with keys: label, file, line_start, line_end.
|
|
51
|
+
line_start and line_end are 0-based indices into script_text.splitlines().
|
|
52
|
+
"""
|
|
53
|
+
lines = script_text.splitlines()
|
|
54
|
+
checkpoints: list[dict[str, Any]] = []
|
|
55
|
+
i = 0
|
|
56
|
+
while i < len(lines):
|
|
57
|
+
if _CP_OPEN_RE.match(lines[i]):
|
|
58
|
+
block_start = i
|
|
59
|
+
label = ""
|
|
60
|
+
file_path = ""
|
|
61
|
+
j = i + 1
|
|
62
|
+
while j < len(lines) and lines[j].strip().startswith("#"):
|
|
63
|
+
m_label = _CP_LABEL_RE.match(lines[j])
|
|
64
|
+
if m_label:
|
|
65
|
+
label = m_label.group(1).strip()
|
|
66
|
+
m_file = re.match(r"^\s*#\s*file:\s*(.+)$", lines[j], re.IGNORECASE)
|
|
67
|
+
if m_file:
|
|
68
|
+
file_path = m_file.group(1).strip()
|
|
69
|
+
j += 1
|
|
70
|
+
checkpoints.append(
|
|
71
|
+
{
|
|
72
|
+
"label": label,
|
|
73
|
+
"file": file_path,
|
|
74
|
+
"line_start": block_start,
|
|
75
|
+
"line_end": j - 1,
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
i = j
|
|
79
|
+
else:
|
|
80
|
+
i += 1
|
|
81
|
+
return checkpoints
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def truncate_script_at(script_text: str, label: str) -> str:
|
|
85
|
+
"""Return the script truncated to (and including) the named checkpoint block.
|
|
86
|
+
|
|
87
|
+
Everything after the checkpoint comment is removed. The result is a valid
|
|
88
|
+
script that can be run to reproduce the model state up to that label.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
script_text: Full generated script text.
|
|
92
|
+
label: Checkpoint label to truncate at (e.g. "base-extrude").
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Truncated script text, or the full script if label not found.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
KeyError: If no checkpoint with that label exists in the script.
|
|
99
|
+
"""
|
|
100
|
+
checkpoints = parse_script_checkpoints(script_text)
|
|
101
|
+
match = next((c for c in checkpoints if c["label"] == label), None)
|
|
102
|
+
if match is None:
|
|
103
|
+
available = [c["label"] for c in checkpoints]
|
|
104
|
+
raise KeyError(
|
|
105
|
+
f"Checkpoint {label!r} not found in script. Available: {available}"
|
|
106
|
+
)
|
|
107
|
+
lines = script_text.splitlines(keepends=True)
|
|
108
|
+
# Include everything up through the closing rule line of the checkpoint block
|
|
109
|
+
end_line = match["line_end"] + 1
|
|
110
|
+
truncated_lines = lines[:end_line]
|
|
111
|
+
# Append the script footer so the truncated version is still runnable
|
|
112
|
+
if not any("await adapter.disconnect()" in ln for ln in truncated_lines):
|
|
113
|
+
truncated_lines.append(
|
|
114
|
+
'\n finally:\n await adapter.disconnect()\n\n\nif __name__ == "__main__":\n asyncio.run(build_part())\n'
|
|
115
|
+
)
|
|
116
|
+
return "".join(truncated_lines)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Model-side rewind (requires adapter)
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def rewind_to_checkpoint(
|
|
125
|
+
adapter: Any,
|
|
126
|
+
session_id: str,
|
|
127
|
+
label: str,
|
|
128
|
+
*,
|
|
129
|
+
db_path: Path | None = None,
|
|
130
|
+
script_text: str | None = None,
|
|
131
|
+
) -> str | None:
|
|
132
|
+
"""Open the checkpoint model in SolidWorks and return the truncated script.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
adapter: Connected SolidWorks adapter (CircuitBreakerAdapter or equivalent).
|
|
136
|
+
session_id: The SoC session ID.
|
|
137
|
+
label: Checkpoint label to rewind to.
|
|
138
|
+
db_path: Override default SQLite DB path.
|
|
139
|
+
script_text: If provided, also truncate this script text at the checkpoint.
|
|
140
|
+
Pass the output of export_session() to get the truncated version back.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Truncated script text if script_text was provided, else None.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
RuntimeError: If the checkpoint is not found in the DB or the file cannot
|
|
147
|
+
be opened.
|
|
148
|
+
"""
|
|
149
|
+
from .history_db import get_soc_checkpoint
|
|
150
|
+
|
|
151
|
+
cp = get_soc_checkpoint(session_id, label, db_path=db_path)
|
|
152
|
+
if cp is None:
|
|
153
|
+
raise RuntimeError(
|
|
154
|
+
f"Checkpoint {label!r} not found for session {session_id!r}. "
|
|
155
|
+
"Run list_soc_checkpoints() to see available labels."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
file_path = cp["file_path"]
|
|
159
|
+
result = await adapter.open_model(file_path)
|
|
160
|
+
if not result.is_success:
|
|
161
|
+
raise RuntimeError(
|
|
162
|
+
f"Failed to open checkpoint file {file_path!r}: {result.error}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if script_text is not None:
|
|
166
|
+
try:
|
|
167
|
+
return truncate_script_at(script_text, label)
|
|
168
|
+
except KeyError:
|
|
169
|
+
# Script doesn't have the comment block yet — return as-is
|
|
170
|
+
return script_text
|
|
171
|
+
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def list_checkpoints(
|
|
176
|
+
session_id: str,
|
|
177
|
+
*,
|
|
178
|
+
db_path: Path | None = None,
|
|
179
|
+
) -> list[dict[str, Any]]:
|
|
180
|
+
"""List all SoCCheckpoints for a session (DB query).
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
session_id: The SoC session ID.
|
|
184
|
+
db_path: Override default SQLite DB path.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Ordered list of checkpoint dicts with keys: id, label, file_path,
|
|
188
|
+
first_record_id, last_record_id, snapshot_id, created_at.
|
|
189
|
+
"""
|
|
190
|
+
from .history_db import list_soc_checkpoints
|
|
191
|
+
|
|
192
|
+
return list_soc_checkpoints(session_id, db_path=db_path)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
# CLI
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _cli() -> None:
|
|
201
|
+
if len(sys.argv) < 3:
|
|
202
|
+
print("Usage: python -m solidworks_mcp.agents.soc_rewind <session_id> <label>")
|
|
203
|
+
sys.exit(1)
|
|
204
|
+
session_id = sys.argv[1]
|
|
205
|
+
label = sys.argv[2]
|
|
206
|
+
checkpoints = list_checkpoints(session_id)
|
|
207
|
+
match = next((c for c in checkpoints if c["label"] == label), None)
|
|
208
|
+
if match is None:
|
|
209
|
+
available = [c["label"] for c in checkpoints]
|
|
210
|
+
print(f"Checkpoint {label!r} not found. Available: {available}")
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
print(f"Checkpoint: {match['label']}")
|
|
213
|
+
print(f" file: {match['file_path']}")
|
|
214
|
+
print(f" records: {match['first_record_id']}–{match['last_record_id']}")
|
|
215
|
+
print(f" created: {match['created_at']}")
|
|
216
|
+
print()
|
|
217
|
+
print(f"To rewind: open {match['file_path']!r} in SolidWorks")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
_cli()
|