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,1476 @@
|
|
|
1
|
+
"""Docs discovery tool for SolidWorks COM and VBA command indexing.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to discover and catalog all available COM objects,
|
|
4
|
+
methods, and properties for the installed SolidWorks version, as well as VBA library
|
|
5
|
+
references. Useful for building context for MCP server operations and enabling
|
|
6
|
+
intelligent tool selection.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import platform
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, TypeVar
|
|
16
|
+
|
|
17
|
+
from loguru import logger
|
|
18
|
+
from pydantic import Field
|
|
19
|
+
|
|
20
|
+
from .input_compat import CompatInput
|
|
21
|
+
from .tool_boundary import (
|
|
22
|
+
ToolOperationError,
|
|
23
|
+
UnsupportedOperationError,
|
|
24
|
+
normalize_input,
|
|
25
|
+
with_tool_boundary,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import win32com.client
|
|
30
|
+
from pywintypes import com_error
|
|
31
|
+
|
|
32
|
+
HAS_WIN32COM = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
HAS_WIN32COM = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Known SolidWorks COM interface catalogue (fallback when typelib unavailable)
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
_KNOWN_SW_INTERFACES: dict[str, dict[str, list[str]]] = {
|
|
41
|
+
"ISldWorks": {
|
|
42
|
+
"methods": [
|
|
43
|
+
"OpenDoc6",
|
|
44
|
+
"CloseDoc",
|
|
45
|
+
"ActivateDoc3",
|
|
46
|
+
"GetActiveObject",
|
|
47
|
+
"GetFirstDocument",
|
|
48
|
+
"GetDocumentCount",
|
|
49
|
+
"NewDocument",
|
|
50
|
+
"SendMsgToUser2",
|
|
51
|
+
"GetProcessID",
|
|
52
|
+
"ExitApp",
|
|
53
|
+
"RevisionNumber",
|
|
54
|
+
"GetUserTypeLibraryFile",
|
|
55
|
+
"CreateNewDocument",
|
|
56
|
+
],
|
|
57
|
+
"properties": ["Visible", "ActiveDoc", "FrameState"],
|
|
58
|
+
},
|
|
59
|
+
"IModelDoc2": {
|
|
60
|
+
"methods": [
|
|
61
|
+
"ShowNamedView2",
|
|
62
|
+
"ViewZoomtofit2",
|
|
63
|
+
"GraphicsRedraw2",
|
|
64
|
+
"SaveBMP",
|
|
65
|
+
"SaveAs3",
|
|
66
|
+
"GetTitle",
|
|
67
|
+
"GetPathName",
|
|
68
|
+
"GetType",
|
|
69
|
+
"GetFeatureCount",
|
|
70
|
+
"GetActiveConfiguration",
|
|
71
|
+
"Extension",
|
|
72
|
+
"SketchManager",
|
|
73
|
+
"FeatureManager",
|
|
74
|
+
"SelectionManager",
|
|
75
|
+
"GetMassProperties",
|
|
76
|
+
"Close",
|
|
77
|
+
"SetSaveAsFileName",
|
|
78
|
+
"ResolveAllLightweightComponents",
|
|
79
|
+
"InsertSketch2",
|
|
80
|
+
"SketchAddConstraints",
|
|
81
|
+
"ClearSelection2",
|
|
82
|
+
],
|
|
83
|
+
"properties": ["ActiveView", "ActiveLayer"],
|
|
84
|
+
},
|
|
85
|
+
"IPartDoc": {
|
|
86
|
+
"methods": [
|
|
87
|
+
"CreateFeatureFromSketch2",
|
|
88
|
+
"AddMateReference",
|
|
89
|
+
"GetBodies2",
|
|
90
|
+
"GetEntityByName",
|
|
91
|
+
"InsertSmartComponents",
|
|
92
|
+
"Material",
|
|
93
|
+
],
|
|
94
|
+
"properties": [],
|
|
95
|
+
},
|
|
96
|
+
"IAssemblyDoc": {
|
|
97
|
+
"methods": [
|
|
98
|
+
"AddComponent4",
|
|
99
|
+
"AddComponent5",
|
|
100
|
+
"InsertNewPart3",
|
|
101
|
+
"LayoutToAssembly",
|
|
102
|
+
"ResolveAllLightweightComponents",
|
|
103
|
+
"AddMate5",
|
|
104
|
+
"GetComponents",
|
|
105
|
+
"GetComponentCount",
|
|
106
|
+
],
|
|
107
|
+
"properties": [],
|
|
108
|
+
},
|
|
109
|
+
"IDrawingDoc": {
|
|
110
|
+
"methods": [
|
|
111
|
+
"CreateDrawViewFromModelView3",
|
|
112
|
+
"InsertModelAnnotations3",
|
|
113
|
+
"GenerateViewPalette",
|
|
114
|
+
"Get3DDrawingView",
|
|
115
|
+
],
|
|
116
|
+
"properties": [],
|
|
117
|
+
},
|
|
118
|
+
"IFeatureManager": {
|
|
119
|
+
"methods": [
|
|
120
|
+
"FeatureExtrusion3",
|
|
121
|
+
"FeatureCut3",
|
|
122
|
+
"FeatureRevolve2",
|
|
123
|
+
"InsertProtrusionBlend4",
|
|
124
|
+
"InsertShell",
|
|
125
|
+
"InsertFillet3",
|
|
126
|
+
"InsertChamfer",
|
|
127
|
+
"InsertDraftXpert3",
|
|
128
|
+
"FeatureMirror3",
|
|
129
|
+
"FeatureCircularPattern2",
|
|
130
|
+
"FeatureLinearPattern3",
|
|
131
|
+
"InsertBoss",
|
|
132
|
+
"FeatureSweep5",
|
|
133
|
+
"FeatureLoft3",
|
|
134
|
+
],
|
|
135
|
+
"properties": [],
|
|
136
|
+
},
|
|
137
|
+
"ISketchManager": {
|
|
138
|
+
"methods": [
|
|
139
|
+
"InsertSketch",
|
|
140
|
+
"CreateLine",
|
|
141
|
+
"CreateArc",
|
|
142
|
+
"CreateCircle",
|
|
143
|
+
"CreateRectangle",
|
|
144
|
+
"CreateEllipse",
|
|
145
|
+
"CreateSpline",
|
|
146
|
+
"CreateCenterLine",
|
|
147
|
+
"SketchMirror",
|
|
148
|
+
"SketchOffset",
|
|
149
|
+
"AddToDB",
|
|
150
|
+
"InsertSketch2",
|
|
151
|
+
],
|
|
152
|
+
"properties": ["ActiveSketch"],
|
|
153
|
+
},
|
|
154
|
+
"IModelView": {
|
|
155
|
+
"methods": [
|
|
156
|
+
"ZoomByFactor",
|
|
157
|
+
"ZoomToSheet",
|
|
158
|
+
"OrientView",
|
|
159
|
+
"SetCameraParameters",
|
|
160
|
+
],
|
|
161
|
+
"properties": ["Orientation3", "Translation3", "FrameWidth", "FrameHeight"],
|
|
162
|
+
},
|
|
163
|
+
"IExtensionManager": {
|
|
164
|
+
"methods": ["SaveAs2", "SelectByID2", "MultiSelect2"],
|
|
165
|
+
"properties": [],
|
|
166
|
+
},
|
|
167
|
+
"IFeature": {
|
|
168
|
+
"methods": [
|
|
169
|
+
"GetNextFeature",
|
|
170
|
+
"GetName",
|
|
171
|
+
"GetTypeName2",
|
|
172
|
+
"SetSuppression2",
|
|
173
|
+
"IsSuppressed2",
|
|
174
|
+
"GetDefinition",
|
|
175
|
+
],
|
|
176
|
+
"properties": ["Name"],
|
|
177
|
+
},
|
|
178
|
+
"IBody2": {
|
|
179
|
+
"methods": [
|
|
180
|
+
"GetFaceCount",
|
|
181
|
+
"GetEdgeCount",
|
|
182
|
+
"GetVertexCount",
|
|
183
|
+
"GetMassProperties",
|
|
184
|
+
"GetType",
|
|
185
|
+
],
|
|
186
|
+
"properties": [],
|
|
187
|
+
},
|
|
188
|
+
"ISketchSegment": {
|
|
189
|
+
"methods": ["GetStartPoint2", "GetEndPoint2", "GetType", "SetEndPoint"],
|
|
190
|
+
"properties": [],
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _enumerate_typeinfo_members(typeinfo: Any) -> tuple[list[str], list[str]]:
|
|
196
|
+
"""Enumerate methods and properties from a COM ITypeInfo object.
|
|
197
|
+
|
|
198
|
+
Uses raw ITypeInfo via pythoncom to walk the function table, which works reliably for
|
|
199
|
+
both early-bound and late-bound COM objects.
|
|
200
|
+
|
|
201
|
+
Returns (methods, properties) as sorted de-duped lists.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
typeinfo (Any): The typeinfo value.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
tuple[list[str], list[str]]: A tuple containing the resulting values.
|
|
208
|
+
"""
|
|
209
|
+
methods: list[str] = []
|
|
210
|
+
properties: list[str] = []
|
|
211
|
+
seen: set[str] = set()
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
typeattr = typeinfo.GetTypeAttr()
|
|
215
|
+
for i in range(typeattr.cFuncs):
|
|
216
|
+
try:
|
|
217
|
+
funcdesc = typeinfo.GetFuncDesc(i)
|
|
218
|
+
names = typeinfo.GetNames(funcdesc.memid)
|
|
219
|
+
name = names[0] if names else None
|
|
220
|
+
if not name or name.startswith("_"):
|
|
221
|
+
continue
|
|
222
|
+
if name in seen:
|
|
223
|
+
continue
|
|
224
|
+
seen.add(name)
|
|
225
|
+
# invkind: 1=FUNC, 2=PROPERTYGET, 4=PROPERTYPUT, 8=PROPERTYPUTREF
|
|
226
|
+
if funcdesc.invkind == 1:
|
|
227
|
+
methods.append(name)
|
|
228
|
+
else:
|
|
229
|
+
properties.append(name)
|
|
230
|
+
except Exception:
|
|
231
|
+
continue
|
|
232
|
+
except Exception:
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
return sorted(methods), sorted(properties)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _discover_com_via_typeinfo(sw_app: Any) -> tuple[dict[str, Any], int, int]:
|
|
239
|
+
"""Enumerate ISldWorks methods/properties via ITypeInfo.
|
|
240
|
+
|
|
241
|
+
Returns a COM index dict keyed by interface name, with the same shape as the rest of the
|
|
242
|
+
index (``methods``, ``properties``, ``method_count``, ``property_count``).
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
sw_app (Any): The sw app value.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
tuple[dict[str, Any], int, int]: com_index, total_methods, total_properties.
|
|
249
|
+
"""
|
|
250
|
+
if not HAS_WIN32COM:
|
|
251
|
+
return {}, 0, 0
|
|
252
|
+
|
|
253
|
+
com_index: dict[str, Any] = {}
|
|
254
|
+
total_methods = 0
|
|
255
|
+
total_properties = 0
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
# GetTypeInfo returns ITypeInfo for the IDispatch interface of the object.
|
|
259
|
+
typeinfo = sw_app._oleobj_.GetTypeInfo()
|
|
260
|
+
methods, properties = _enumerate_typeinfo_members(typeinfo)
|
|
261
|
+
|
|
262
|
+
# Walk implemented interfaces to catch inherited members.
|
|
263
|
+
try:
|
|
264
|
+
typeattr = typeinfo.GetTypeAttr()
|
|
265
|
+
for impl_idx in range(typeattr.cImplTypes):
|
|
266
|
+
try:
|
|
267
|
+
ref_type = typeinfo.GetRefTypeOfImplType(impl_idx)
|
|
268
|
+
ref_typeinfo = typeinfo.GetRefTypeInfo(ref_type)
|
|
269
|
+
extra_m, extra_p = _enumerate_typeinfo_members(ref_typeinfo)
|
|
270
|
+
for m in extra_m:
|
|
271
|
+
if m not in methods:
|
|
272
|
+
methods.append(m)
|
|
273
|
+
for p in extra_p:
|
|
274
|
+
if p not in properties:
|
|
275
|
+
properties.append(p)
|
|
276
|
+
except Exception:
|
|
277
|
+
continue
|
|
278
|
+
except Exception:
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
methods = sorted(set(methods))
|
|
282
|
+
properties = sorted(set(properties))
|
|
283
|
+
|
|
284
|
+
if methods or properties:
|
|
285
|
+
com_index["ISldWorks"] = {
|
|
286
|
+
"methods": methods,
|
|
287
|
+
"properties": properties,
|
|
288
|
+
"method_count": len(methods),
|
|
289
|
+
"property_count": len(properties),
|
|
290
|
+
}
|
|
291
|
+
total_methods += len(methods)
|
|
292
|
+
total_properties += len(properties)
|
|
293
|
+
|
|
294
|
+
except Exception as exc:
|
|
295
|
+
logger.debug(
|
|
296
|
+
"[docs_discovery] ITypeInfo enumeration unavailable; using built-in interface catalog fallback: {}",
|
|
297
|
+
exc,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
return com_index, total_methods, total_properties
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _discover_vba_references_via_registry() -> dict[str, Any]:
|
|
304
|
+
"""Enumerate VBA/TypeLib references via the Windows Registry.
|
|
305
|
+
|
|
306
|
+
Scans HKEY_CLASSES_ROOT\\TypeLib for entries whose name or GUID matches SolidWorks or
|
|
307
|
+
common Office/VBA libraries.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
311
|
+
"""
|
|
312
|
+
vba_refs: dict[str, Any] = {}
|
|
313
|
+
|
|
314
|
+
# Keywords to look for in type lib names
|
|
315
|
+
interesting_keywords = [
|
|
316
|
+
"solidworks",
|
|
317
|
+
"sldworks",
|
|
318
|
+
"vba",
|
|
319
|
+
"visual basic",
|
|
320
|
+
"stdole",
|
|
321
|
+
"office",
|
|
322
|
+
"microsoft office",
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
if platform.system() != "Windows":
|
|
326
|
+
return vba_refs
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
import winreg
|
|
330
|
+
|
|
331
|
+
typelib_key = winreg.OpenKey(
|
|
332
|
+
winreg.HKEY_CLASSES_ROOT, "TypeLib", access=winreg.KEY_READ
|
|
333
|
+
)
|
|
334
|
+
idx = 0
|
|
335
|
+
while True:
|
|
336
|
+
try:
|
|
337
|
+
guid = winreg.EnumKey(typelib_key, idx)
|
|
338
|
+
idx += 1
|
|
339
|
+
except OSError:
|
|
340
|
+
break
|
|
341
|
+
try:
|
|
342
|
+
guid_key = winreg.OpenKey(typelib_key, guid, access=winreg.KEY_READ)
|
|
343
|
+
ver_idx = 0
|
|
344
|
+
while True:
|
|
345
|
+
try:
|
|
346
|
+
version = winreg.EnumKey(guid_key, ver_idx)
|
|
347
|
+
ver_idx += 1
|
|
348
|
+
except OSError:
|
|
349
|
+
break
|
|
350
|
+
try:
|
|
351
|
+
ver_key = winreg.OpenKey(
|
|
352
|
+
guid_key, version, access=winreg.KEY_READ
|
|
353
|
+
)
|
|
354
|
+
try:
|
|
355
|
+
name, _ = winreg.QueryValueEx(ver_key, "")
|
|
356
|
+
except OSError:
|
|
357
|
+
name = ""
|
|
358
|
+
winreg.CloseKey(ver_key)
|
|
359
|
+
if name and any(
|
|
360
|
+
kw in name.lower() for kw in interesting_keywords
|
|
361
|
+
):
|
|
362
|
+
key = f"{name} ({guid} v{version})"
|
|
363
|
+
if key not in vba_refs:
|
|
364
|
+
vba_refs[key] = {
|
|
365
|
+
"description": name,
|
|
366
|
+
"guid": guid,
|
|
367
|
+
"version": version,
|
|
368
|
+
"status": "available",
|
|
369
|
+
}
|
|
370
|
+
except Exception:
|
|
371
|
+
continue
|
|
372
|
+
winreg.CloseKey(guid_key)
|
|
373
|
+
except Exception:
|
|
374
|
+
continue
|
|
375
|
+
winreg.CloseKey(typelib_key)
|
|
376
|
+
except Exception as exc:
|
|
377
|
+
logger.debug("[docs_discovery] Registry TypeLib scan failed: {}", exc)
|
|
378
|
+
|
|
379
|
+
return vba_refs
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class SolidWorksDocsDiscovery:
|
|
383
|
+
"""Discover and index SolidWorks COM and VBA documentation.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
output_dir (Path | None): The output dir value. Defaults to None.
|
|
387
|
+
|
|
388
|
+
Attributes:
|
|
389
|
+
output_dir (Any): The output dir value.
|
|
390
|
+
sw_app (Any): The sw app value.
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
def __init__(self, output_dir: Path | None = None):
|
|
394
|
+
"""Initialize docs discovery.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
output_dir (Path | None): The output dir value. Defaults to None.
|
|
398
|
+
"""
|
|
399
|
+
self.output_dir = output_dir or Path(".generated/docs-index")
|
|
400
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
401
|
+
self.sw_app: Any = None
|
|
402
|
+
self.index: dict[str, Any] = {
|
|
403
|
+
"version": "1.0",
|
|
404
|
+
"solidworks_version": None,
|
|
405
|
+
"com_objects": {},
|
|
406
|
+
"vba_references": {},
|
|
407
|
+
"total_methods": 0,
|
|
408
|
+
"total_properties": 0,
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
def connect_to_solidworks(self) -> bool:
|
|
412
|
+
"""Attach to an already-running SolidWorks instance.
|
|
413
|
+
|
|
414
|
+
Discovery only ever attaches; it never starts SolidWorks. Launching
|
|
415
|
+
from a process outside the desktop session (an SSH shell, a test run,
|
|
416
|
+
a service) starts a hidden SLDWORKS.exe nobody can see or close, so
|
|
417
|
+
when no instance is running this returns False instead.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
bool: True if attached to a running SolidWorks, otherwise False.
|
|
421
|
+
"""
|
|
422
|
+
if not HAS_WIN32COM:
|
|
423
|
+
logger.error("win32com not available; cannot index COM")
|
|
424
|
+
return False
|
|
425
|
+
|
|
426
|
+
if platform.system() != "Windows":
|
|
427
|
+
logger.error("COM discovery only available on Windows")
|
|
428
|
+
return False
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
self.sw_app = win32com.client.GetActiveObject("SldWorks.Application")
|
|
432
|
+
return True
|
|
433
|
+
except com_error as e:
|
|
434
|
+
logger.error(
|
|
435
|
+
"SolidWorks is not running in this session; start it and retry "
|
|
436
|
+
f"(docs discovery never launches SolidWorks): {e}"
|
|
437
|
+
)
|
|
438
|
+
return False
|
|
439
|
+
|
|
440
|
+
def discover_com_objects(self) -> dict[str, Any]:
|
|
441
|
+
"""Discover all COM objects and their methods/properties.
|
|
442
|
+
|
|
443
|
+
Uses ITypeInfo enumeration via pythoncom for ISldWorks, then supplements with the active
|
|
444
|
+
document's interface if a model is open. Falls back to the known-interface catalogue
|
|
445
|
+
when typelib introspection is unavailable.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
449
|
+
"""
|
|
450
|
+
if not self.sw_app:
|
|
451
|
+
logger.error("Not connected to SolidWorks")
|
|
452
|
+
return {}
|
|
453
|
+
|
|
454
|
+
com_index: dict[str, Any] = {}
|
|
455
|
+
total_m = 0
|
|
456
|
+
total_p = 0
|
|
457
|
+
|
|
458
|
+
# ------------------------------------------------------------------ #
|
|
459
|
+
# Primary: ITypeInfo enumeration for ISldWorks
|
|
460
|
+
# ------------------------------------------------------------------ #
|
|
461
|
+
if HAS_WIN32COM:
|
|
462
|
+
try:
|
|
463
|
+
result = _discover_com_via_typeinfo(self.sw_app)
|
|
464
|
+
# _discover_com_via_typeinfo returns (dict, total_m, total_p)
|
|
465
|
+
partial_index, t_m, t_p = result
|
|
466
|
+
com_index.update(partial_index)
|
|
467
|
+
total_m += t_m
|
|
468
|
+
total_p += t_p
|
|
469
|
+
if partial_index:
|
|
470
|
+
logger.info(
|
|
471
|
+
"[docs_discovery] ITypeInfo: {} ISldWorks methods, {} properties",
|
|
472
|
+
t_m,
|
|
473
|
+
t_p,
|
|
474
|
+
)
|
|
475
|
+
except Exception as exc:
|
|
476
|
+
logger.debug("[docs_discovery] ITypeInfo path failed: {}", exc)
|
|
477
|
+
|
|
478
|
+
# ------------------------------------------------------------------ #
|
|
479
|
+
# Supplement: active document (IModelDoc2) if a model is open
|
|
480
|
+
# ------------------------------------------------------------------ #
|
|
481
|
+
try:
|
|
482
|
+
active_doc = self.sw_app.ActiveDoc
|
|
483
|
+
if active_doc is not None and HAS_WIN32COM:
|
|
484
|
+
doc_typeinfo = active_doc._oleobj_.GetTypeInfo()
|
|
485
|
+
doc_methods, doc_props = _enumerate_typeinfo_members(doc_typeinfo)
|
|
486
|
+
if doc_methods or doc_props:
|
|
487
|
+
com_index["IModelDoc2_active"] = {
|
|
488
|
+
"methods": doc_methods,
|
|
489
|
+
"properties": doc_props,
|
|
490
|
+
"method_count": len(doc_methods),
|
|
491
|
+
"property_count": len(doc_props),
|
|
492
|
+
}
|
|
493
|
+
total_m += len(doc_methods)
|
|
494
|
+
total_p += len(doc_props)
|
|
495
|
+
logger.info(
|
|
496
|
+
"[docs_discovery] Active doc typeinfo: {} methods, {} properties",
|
|
497
|
+
len(doc_methods),
|
|
498
|
+
len(doc_props),
|
|
499
|
+
)
|
|
500
|
+
except Exception:
|
|
501
|
+
pass
|
|
502
|
+
|
|
503
|
+
# ------------------------------------------------------------------ #
|
|
504
|
+
# Fallback: merge known-interface catalogue for any gaps
|
|
505
|
+
# ------------------------------------------------------------------ #
|
|
506
|
+
for iface_name, members in _KNOWN_SW_INTERFACES.items():
|
|
507
|
+
if iface_name in com_index:
|
|
508
|
+
continue # already populated via typeinfo
|
|
509
|
+
com_index[iface_name] = {
|
|
510
|
+
"methods": members["methods"],
|
|
511
|
+
"properties": members["properties"],
|
|
512
|
+
"method_count": len(members["methods"]),
|
|
513
|
+
"property_count": len(members["properties"]),
|
|
514
|
+
}
|
|
515
|
+
total_m += len(members["methods"])
|
|
516
|
+
total_p += len(members["properties"])
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
self.index["total_methods"] += total_m
|
|
520
|
+
self.index["total_properties"] += total_p
|
|
521
|
+
except TypeError:
|
|
522
|
+
pass # accumulator corrupted (e.g. set to a string by tests); tolerate
|
|
523
|
+
|
|
524
|
+
# Get SolidWorks version
|
|
525
|
+
try:
|
|
526
|
+
self.index["solidworks_version"] = self.sw_app.RevisionNumber()
|
|
527
|
+
except Exception:
|
|
528
|
+
pass
|
|
529
|
+
|
|
530
|
+
return com_index
|
|
531
|
+
|
|
532
|
+
def discover_vba_references(self) -> dict[str, Any]:
|
|
533
|
+
"""Discover VBA/TypeLib references via the Windows Registry.
|
|
534
|
+
|
|
535
|
+
Scans HKEY_CLASSES_ROOT\\TypeLib for SolidWorks and common Office/VBA type libraries.
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
539
|
+
"""
|
|
540
|
+
refs = _discover_vba_references_via_registry()
|
|
541
|
+
if refs:
|
|
542
|
+
logger.info(
|
|
543
|
+
"[docs_discovery] Registry TypeLib scan found {} entries", len(refs)
|
|
544
|
+
)
|
|
545
|
+
else:
|
|
546
|
+
logger.debug(
|
|
547
|
+
"[docs_discovery] Registry TypeLib scan found no matching entries"
|
|
548
|
+
)
|
|
549
|
+
return refs
|
|
550
|
+
|
|
551
|
+
def discover_all(self) -> dict[str, Any]:
|
|
552
|
+
"""Run full discovery of COM and VBA documentation.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
556
|
+
"""
|
|
557
|
+
if not self.connect_to_solidworks():
|
|
558
|
+
logger.error("Cannot proceed with discovery without SolidWorks connection")
|
|
559
|
+
return self.index
|
|
560
|
+
|
|
561
|
+
logger.info("Discovering COM objects...")
|
|
562
|
+
self.index["com_objects"] = self.discover_com_objects()
|
|
563
|
+
|
|
564
|
+
logger.info("Discovering VBA references...")
|
|
565
|
+
self.index["vba_references"] = self.discover_vba_references()
|
|
566
|
+
|
|
567
|
+
return self.index
|
|
568
|
+
|
|
569
|
+
def save_index(self, filename: str = "solidworks_docs_index.json") -> Path | None:
|
|
570
|
+
"""Save discovered documentation to JSON file.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
filename (str): The filename value. Defaults to "solidworks_docs_index.json".
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
Path | None: The result produced by the operation.
|
|
577
|
+
"""
|
|
578
|
+
try:
|
|
579
|
+
output_path = self.output_dir / filename
|
|
580
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
581
|
+
json.dump(self.index, f, indent=2)
|
|
582
|
+
logger.info(f"Docs index saved to {output_path}")
|
|
583
|
+
return output_path
|
|
584
|
+
except Exception as e:
|
|
585
|
+
logger.error(f"Failed to save docs index: {e}")
|
|
586
|
+
return None
|
|
587
|
+
|
|
588
|
+
def create_search_summary(self) -> dict[str, Any]:
|
|
589
|
+
"""Create a summary of indexed documentation for search/reference.
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
593
|
+
"""
|
|
594
|
+
summary = {
|
|
595
|
+
"total_com_objects": len(self.index["com_objects"]),
|
|
596
|
+
"total_methods": self.index["total_methods"],
|
|
597
|
+
"total_properties": self.index["total_properties"],
|
|
598
|
+
"solidworks_version": self.index["solidworks_version"],
|
|
599
|
+
"available_vba_libs": [
|
|
600
|
+
lib
|
|
601
|
+
for lib, info in self.index["vba_references"].items()
|
|
602
|
+
if info.get("status") == "available"
|
|
603
|
+
],
|
|
604
|
+
}
|
|
605
|
+
return summary
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
class DiscoverDocsInput(CompatInput):
|
|
609
|
+
"""Input schema for docs discovery.
|
|
610
|
+
|
|
611
|
+
Attributes:
|
|
612
|
+
include_vba (bool): The include vba value.
|
|
613
|
+
output_dir (str | None): The output dir value.
|
|
614
|
+
year (int | None): The year value.
|
|
615
|
+
"""
|
|
616
|
+
|
|
617
|
+
output_dir: str | None = Field(
|
|
618
|
+
default=None,
|
|
619
|
+
description="Optional output directory for indexed documentation",
|
|
620
|
+
)
|
|
621
|
+
include_vba: bool = Field(
|
|
622
|
+
default=True,
|
|
623
|
+
description="Include VBA library reference indexing",
|
|
624
|
+
)
|
|
625
|
+
year: int | None = Field(
|
|
626
|
+
default=None,
|
|
627
|
+
description="SolidWorks year override for saved index naming (e.g., 2026)",
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
class SearchApiHelpInput(CompatInput):
|
|
632
|
+
"""Input schema for SolidWorks API help search.
|
|
633
|
+
|
|
634
|
+
Attributes:
|
|
635
|
+
auto_discover_if_missing (bool): The auto discover if missing value.
|
|
636
|
+
index_file (str | None): The index file value.
|
|
637
|
+
max_results (int): The max results value.
|
|
638
|
+
query (str): The query value.
|
|
639
|
+
year (int | None): The year value.
|
|
640
|
+
"""
|
|
641
|
+
|
|
642
|
+
query: str = Field(
|
|
643
|
+
description="Search phrase for SolidWorks API help (methods, properties, objects)",
|
|
644
|
+
min_length=2,
|
|
645
|
+
)
|
|
646
|
+
year: int | None = Field(
|
|
647
|
+
default=None,
|
|
648
|
+
description="SolidWorks year override (e.g., 2025, 2026)",
|
|
649
|
+
)
|
|
650
|
+
max_results: int = Field(
|
|
651
|
+
default=10,
|
|
652
|
+
ge=1,
|
|
653
|
+
le=50,
|
|
654
|
+
description="Maximum number of search results",
|
|
655
|
+
)
|
|
656
|
+
index_file: str | None = Field(
|
|
657
|
+
default=None,
|
|
658
|
+
description="Optional explicit index file path (JSON) to search",
|
|
659
|
+
)
|
|
660
|
+
auto_discover_if_missing: bool = Field(
|
|
661
|
+
default=False,
|
|
662
|
+
description="Generate docs index first when no index file is found",
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
class LookupApiMethodInput(CompatInput):
|
|
667
|
+
"""Input schema for a single method/property lookup."""
|
|
668
|
+
|
|
669
|
+
interface: str = Field(
|
|
670
|
+
description="COM interface name, e.g. 'IFeatureManager'", min_length=2
|
|
671
|
+
)
|
|
672
|
+
method: str = Field(
|
|
673
|
+
description="Method or property name, e.g. 'InsertFeatureChamfer'",
|
|
674
|
+
min_length=1,
|
|
675
|
+
)
|
|
676
|
+
year: int | None = Field(default=None, description="SolidWorks year override")
|
|
677
|
+
index_file: str | None = Field(
|
|
678
|
+
default=None, description="Optional explicit index file path (JSON)"
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
class LookupApiInterfaceInput(CompatInput):
|
|
683
|
+
"""Input schema for an interface member-list lookup."""
|
|
684
|
+
|
|
685
|
+
interface: str = Field(
|
|
686
|
+
description="COM interface name, e.g. 'ISldWorks'", min_length=2
|
|
687
|
+
)
|
|
688
|
+
year: int | None = Field(default=None, description="SolidWorks year override")
|
|
689
|
+
index_file: str | None = Field(
|
|
690
|
+
default=None, description="Optional explicit index file path (JSON)"
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
class FindRelatedApiInput(CompatInput):
|
|
695
|
+
"""Input schema for a related-members lookup."""
|
|
696
|
+
|
|
697
|
+
name: str = Field(
|
|
698
|
+
description="An indexed interface or member name to find relatives of",
|
|
699
|
+
min_length=2,
|
|
700
|
+
)
|
|
701
|
+
year: int | None = Field(default=None, description="SolidWorks year override")
|
|
702
|
+
max_results: int = Field(
|
|
703
|
+
default=25, ge=1, le=200, description="Maximum related members to return"
|
|
704
|
+
)
|
|
705
|
+
index_file: str | None = Field(
|
|
706
|
+
default=None, description="Optional explicit index file path (JSON)"
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
CompatInputT = TypeVar("CompatInputT", bound=CompatInput)
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _extract_year(value: str | None) -> int | None:
|
|
714
|
+
"""Extract a 4-digit year from any string.
|
|
715
|
+
|
|
716
|
+
Args:
|
|
717
|
+
value (str | None): The value value.
|
|
718
|
+
|
|
719
|
+
Returns:
|
|
720
|
+
int | None: The result produced by the operation.
|
|
721
|
+
"""
|
|
722
|
+
if not value:
|
|
723
|
+
return None
|
|
724
|
+
match = re.search(r"\b(20\d{2})\b", value)
|
|
725
|
+
if not match:
|
|
726
|
+
return None
|
|
727
|
+
try:
|
|
728
|
+
return int(match.group(1))
|
|
729
|
+
except ValueError:
|
|
730
|
+
return None
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _detect_installed_solidworks_year() -> int | None:
|
|
734
|
+
"""Detect the latest installed SolidWorks year from the public samples path.
|
|
735
|
+
|
|
736
|
+
Returns:
|
|
737
|
+
int | None: The result produced by the operation.
|
|
738
|
+
"""
|
|
739
|
+
root = Path(r"C:\Users\Public\Documents\SOLIDWORKS")
|
|
740
|
+
if not root.exists():
|
|
741
|
+
return None
|
|
742
|
+
|
|
743
|
+
years: list[int] = []
|
|
744
|
+
try:
|
|
745
|
+
for child in root.iterdir():
|
|
746
|
+
if not child.is_dir():
|
|
747
|
+
continue
|
|
748
|
+
year = _extract_year(child.name)
|
|
749
|
+
if year is not None:
|
|
750
|
+
years.append(year)
|
|
751
|
+
except OSError:
|
|
752
|
+
# Path can be inaccessible or missing on CI runners and non-Windows hosts.
|
|
753
|
+
return None
|
|
754
|
+
|
|
755
|
+
if not years:
|
|
756
|
+
return None
|
|
757
|
+
return max(years)
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _resolve_solidworks_year(requested_year: int | None, config: Any) -> int | None:
|
|
761
|
+
"""Resolve SolidWorks year from explicit request, config, then local installation.
|
|
762
|
+
|
|
763
|
+
Args:
|
|
764
|
+
requested_year (int | None): The requested year value.
|
|
765
|
+
config (Any): Configuration values for the operation.
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
int | None: The result produced by the operation.
|
|
769
|
+
"""
|
|
770
|
+
if requested_year:
|
|
771
|
+
return requested_year
|
|
772
|
+
|
|
773
|
+
config_year = getattr(config, "solidworks_year", None)
|
|
774
|
+
if isinstance(config_year, int):
|
|
775
|
+
return config_year
|
|
776
|
+
|
|
777
|
+
config_path_year = _extract_year(getattr(config, "solidworks_path", None))
|
|
778
|
+
if config_path_year:
|
|
779
|
+
return config_path_year
|
|
780
|
+
|
|
781
|
+
return _detect_installed_solidworks_year()
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _load_index_file(index_file: Path) -> dict[str, Any] | None:
|
|
785
|
+
"""Load docs index JSON from disk.
|
|
786
|
+
|
|
787
|
+
Args:
|
|
788
|
+
index_file (Path): The index file value.
|
|
789
|
+
|
|
790
|
+
Returns:
|
|
791
|
+
dict[str, Any] | None: A dictionary containing the resulting values.
|
|
792
|
+
"""
|
|
793
|
+
try:
|
|
794
|
+
if not index_file.exists() or not index_file.is_file():
|
|
795
|
+
return None
|
|
796
|
+
with index_file.open("r", encoding="utf-8") as handle:
|
|
797
|
+
data = json.load(handle)
|
|
798
|
+
if isinstance(data, dict):
|
|
799
|
+
return data
|
|
800
|
+
return None
|
|
801
|
+
except Exception:
|
|
802
|
+
return None
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _find_index_file(year: int | None, explicit_index_file: str | None) -> Path | None:
|
|
806
|
+
"""Find the most appropriate index file for a requested year.
|
|
807
|
+
|
|
808
|
+
Args:
|
|
809
|
+
year (int | None): The year value.
|
|
810
|
+
explicit_index_file (str | None): The explicit index file value.
|
|
811
|
+
|
|
812
|
+
Returns:
|
|
813
|
+
Path | None: The result produced by the operation.
|
|
814
|
+
"""
|
|
815
|
+
if explicit_index_file:
|
|
816
|
+
explicit = Path(explicit_index_file)
|
|
817
|
+
if explicit.exists():
|
|
818
|
+
return explicit
|
|
819
|
+
|
|
820
|
+
names = ["solidworks_docs_index.json"]
|
|
821
|
+
if year:
|
|
822
|
+
names.insert(0, f"solidworks_docs_index_{year}.json")
|
|
823
|
+
|
|
824
|
+
search_dirs = [
|
|
825
|
+
Path(".generated/docs-index"),
|
|
826
|
+
Path("tests/.generated/solidworks_integration/docs-index"),
|
|
827
|
+
Path("tests/.generated/docs-index"),
|
|
828
|
+
]
|
|
829
|
+
|
|
830
|
+
for directory in search_dirs:
|
|
831
|
+
for name in names:
|
|
832
|
+
candidate = directory / name
|
|
833
|
+
if candidate.exists():
|
|
834
|
+
return candidate
|
|
835
|
+
|
|
836
|
+
return None
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def _index_is_stale(index_file: Path | None, max_age_days: int) -> bool:
|
|
840
|
+
"""True when an existing index file is older than ``max_age_days``."""
|
|
841
|
+
if index_file is None:
|
|
842
|
+
return False
|
|
843
|
+
try:
|
|
844
|
+
if not index_file.exists():
|
|
845
|
+
return False
|
|
846
|
+
import time
|
|
847
|
+
|
|
848
|
+
age_days = (time.time() - index_file.stat().st_mtime) / 86400.0
|
|
849
|
+
return age_days > max_age_days
|
|
850
|
+
except OSError:
|
|
851
|
+
return False
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def _maybe_refresh_index(
|
|
855
|
+
index: dict[str, Any] | None,
|
|
856
|
+
index_file: Path | None,
|
|
857
|
+
year: int | None,
|
|
858
|
+
config: Any,
|
|
859
|
+
) -> tuple[dict[str, Any] | None, Path | None]:
|
|
860
|
+
"""Rebuild the docs index if an existing one is stale and the config allows.
|
|
861
|
+
|
|
862
|
+
Only acts on an index file that exists but is older than
|
|
863
|
+
``docs_index_max_age_days`` — a *missing* index is left to the caller's
|
|
864
|
+
``auto_discover_if_missing`` opt-in. Returns the (possibly refreshed)
|
|
865
|
+
``(index, index_file)``. A no-op without win32com / SolidWorks, or when
|
|
866
|
+
``config.docs_index_auto_refresh`` is off; any failure leaves the original
|
|
867
|
+
index untouched.
|
|
868
|
+
"""
|
|
869
|
+
if not getattr(config, "docs_index_auto_refresh", False):
|
|
870
|
+
return index, index_file
|
|
871
|
+
max_age = int(getattr(config, "docs_index_max_age_days", 21) or 21)
|
|
872
|
+
if not _index_is_stale(index_file, max_age):
|
|
873
|
+
return index, index_file
|
|
874
|
+
if not (HAS_WIN32COM and platform.system() == "Windows"):
|
|
875
|
+
return index, index_file
|
|
876
|
+
try:
|
|
877
|
+
logger.info(
|
|
878
|
+
"Docs index missing or older than {} days; rebuilding via "
|
|
879
|
+
"discover_solidworks_docs",
|
|
880
|
+
max_age,
|
|
881
|
+
)
|
|
882
|
+
discovery = SolidWorksDocsDiscovery()
|
|
883
|
+
fresh = discovery.discover_all()
|
|
884
|
+
if discovery.sw_app is None:
|
|
885
|
+
return index, index_file
|
|
886
|
+
filename = (
|
|
887
|
+
f"solidworks_docs_index_{year}.json"
|
|
888
|
+
if year
|
|
889
|
+
else "solidworks_docs_index.json"
|
|
890
|
+
)
|
|
891
|
+
fresh_file = discovery.save_index(filename=filename)
|
|
892
|
+
return fresh, fresh_file or index_file
|
|
893
|
+
except Exception as exc: # noqa: BLE001 - keep serving the stale index
|
|
894
|
+
logger.warning("Docs index auto-refresh failed, using existing index: {}", exc)
|
|
895
|
+
return index, index_file
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def _search_index(
|
|
899
|
+
index: dict[str, Any], query: str, max_results: int
|
|
900
|
+
) -> list[dict[str, Any]]:
|
|
901
|
+
"""Search indexed COM objects/members for a query.
|
|
902
|
+
|
|
903
|
+
Args:
|
|
904
|
+
index (dict[str, Any]): The index value.
|
|
905
|
+
query (str): Query text used for the operation.
|
|
906
|
+
max_results (int): The max results value.
|
|
907
|
+
|
|
908
|
+
Returns:
|
|
909
|
+
list[dict[str, Any]]: A list containing the resulting items.
|
|
910
|
+
"""
|
|
911
|
+
tokens = [t for t in re.split(r"\s+", query.lower().strip()) if t]
|
|
912
|
+
if not tokens:
|
|
913
|
+
return []
|
|
914
|
+
|
|
915
|
+
results: list[dict[str, Any]] = []
|
|
916
|
+
|
|
917
|
+
def _score(text: str) -> int:
|
|
918
|
+
"""Build internal score.
|
|
919
|
+
|
|
920
|
+
Args:
|
|
921
|
+
text (str): Input text processed by the operation.
|
|
922
|
+
|
|
923
|
+
Returns:
|
|
924
|
+
int: The computed numeric result.
|
|
925
|
+
"""
|
|
926
|
+
lower = text.lower()
|
|
927
|
+
score = 0
|
|
928
|
+
for token in tokens:
|
|
929
|
+
if token == lower:
|
|
930
|
+
score += 10
|
|
931
|
+
elif token in lower:
|
|
932
|
+
score += 3
|
|
933
|
+
return score
|
|
934
|
+
|
|
935
|
+
for obj_name, obj_data in index.get("com_objects", {}).items():
|
|
936
|
+
obj_score = _score(obj_name)
|
|
937
|
+
|
|
938
|
+
for method in obj_data.get("methods", []):
|
|
939
|
+
score = obj_score + _score(str(method))
|
|
940
|
+
if score > 0:
|
|
941
|
+
results.append(
|
|
942
|
+
{
|
|
943
|
+
"object": obj_name,
|
|
944
|
+
"member": method,
|
|
945
|
+
"member_type": "method",
|
|
946
|
+
"score": score,
|
|
947
|
+
}
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
for prop in obj_data.get("properties", []):
|
|
951
|
+
score = obj_score + _score(str(prop))
|
|
952
|
+
if score > 0:
|
|
953
|
+
results.append(
|
|
954
|
+
{
|
|
955
|
+
"object": obj_name,
|
|
956
|
+
"member": prop,
|
|
957
|
+
"member_type": "property",
|
|
958
|
+
"score": score,
|
|
959
|
+
}
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
for lib_name, lib_data in index.get("vba_references", {}).items():
|
|
963
|
+
lib_text = f"{lib_name} {lib_data.get('description', '')}"
|
|
964
|
+
score = _score(lib_text)
|
|
965
|
+
if score > 0:
|
|
966
|
+
results.append(
|
|
967
|
+
{
|
|
968
|
+
"object": "VBA Library",
|
|
969
|
+
"member": lib_name,
|
|
970
|
+
"member_type": "reference",
|
|
971
|
+
"status": lib_data.get("status", "unknown"),
|
|
972
|
+
"score": score,
|
|
973
|
+
}
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
results.sort(key=lambda item: item.get("score", 0), reverse=True)
|
|
977
|
+
return results[:max_results]
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
def _resolve_interface(
|
|
981
|
+
index: dict[str, Any], name: str
|
|
982
|
+
) -> tuple[str, dict[str, Any]] | None:
|
|
983
|
+
"""Case-insensitively resolve an interface name to its (canonical_name, data)."""
|
|
984
|
+
com_objects = index.get("com_objects", {})
|
|
985
|
+
if name in com_objects:
|
|
986
|
+
return name, com_objects[name]
|
|
987
|
+
lowered = name.lower()
|
|
988
|
+
for obj_name, obj_data in com_objects.items():
|
|
989
|
+
if obj_name.lower() == lowered:
|
|
990
|
+
return obj_name, obj_data
|
|
991
|
+
return None
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def _lookup_interface(index: dict[str, Any], interface: str) -> dict[str, Any]:
|
|
995
|
+
"""Return every indexed member of an interface, or a no-match marker.
|
|
996
|
+
|
|
997
|
+
The index stores member *names* only (no signatures), so this returns the
|
|
998
|
+
full method/property lists for the interface and nothing more.
|
|
999
|
+
"""
|
|
1000
|
+
resolved = _resolve_interface(index, interface)
|
|
1001
|
+
if resolved is None:
|
|
1002
|
+
return {"found": False, "interface": interface}
|
|
1003
|
+
obj_name, obj_data = resolved
|
|
1004
|
+
methods = [str(m) for m in obj_data.get("methods", [])]
|
|
1005
|
+
properties = [str(p) for p in obj_data.get("properties", [])]
|
|
1006
|
+
return {
|
|
1007
|
+
"found": True,
|
|
1008
|
+
"interface": obj_name,
|
|
1009
|
+
"methods": methods,
|
|
1010
|
+
"properties": properties,
|
|
1011
|
+
"method_count": len(methods),
|
|
1012
|
+
"property_count": len(properties),
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def _lookup_method(
|
|
1017
|
+
index: dict[str, Any], interface: str, method: str
|
|
1018
|
+
) -> dict[str, Any]:
|
|
1019
|
+
"""Confirm a method/property is indexed on an interface.
|
|
1020
|
+
|
|
1021
|
+
The index has no signatures, so ``parameters`` / ``return_type`` are always
|
|
1022
|
+
reported as not indexed; the useful output is confirmation the member
|
|
1023
|
+
exists on that interface plus its sibling members for context.
|
|
1024
|
+
"""
|
|
1025
|
+
resolved = _resolve_interface(index, interface)
|
|
1026
|
+
if resolved is None:
|
|
1027
|
+
return {"found": False, "interface": interface, "method": method}
|
|
1028
|
+
obj_name, obj_data = resolved
|
|
1029
|
+
lowered = method.lower()
|
|
1030
|
+
for member_type, key in (("method", "methods"), ("property", "properties")):
|
|
1031
|
+
for member in obj_data.get(key, []):
|
|
1032
|
+
if str(member).lower() == lowered:
|
|
1033
|
+
return {
|
|
1034
|
+
"found": True,
|
|
1035
|
+
"interface": obj_name,
|
|
1036
|
+
"member": str(member),
|
|
1037
|
+
"member_type": member_type,
|
|
1038
|
+
"parameters": None,
|
|
1039
|
+
"return_type": None,
|
|
1040
|
+
"signature_indexed": False,
|
|
1041
|
+
"note": (
|
|
1042
|
+
"The local docs index stores member names only. Run "
|
|
1043
|
+
"search_solidworks_api_help or consult help.solidworks.com "
|
|
1044
|
+
"for the full signature."
|
|
1045
|
+
),
|
|
1046
|
+
}
|
|
1047
|
+
return {
|
|
1048
|
+
"found": False,
|
|
1049
|
+
"interface": obj_name,
|
|
1050
|
+
"method": method,
|
|
1051
|
+
"note": f"{method!r} is not an indexed member of {obj_name}.",
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def _find_related_api_members(
|
|
1056
|
+
index: dict[str, Any], name: str, max_results: int
|
|
1057
|
+
) -> dict[str, Any]:
|
|
1058
|
+
"""Given an indexed interface or member name, return related members.
|
|
1059
|
+
|
|
1060
|
+
- An interface name -> its own members.
|
|
1061
|
+
- A member name -> the other members of every interface that also exposes
|
|
1062
|
+
it (siblings commonly used together), plus name-substring matches
|
|
1063
|
+
elsewhere in the index.
|
|
1064
|
+
"""
|
|
1065
|
+
com_objects = index.get("com_objects", {})
|
|
1066
|
+
related: list[dict[str, Any]] = []
|
|
1067
|
+
seen: set[tuple[str, str]] = set()
|
|
1068
|
+
|
|
1069
|
+
def _add(obj_name: str, member: str, member_type: str, reason: str) -> None:
|
|
1070
|
+
key = (obj_name, str(member))
|
|
1071
|
+
if key in seen or str(member).lower() == name.lower():
|
|
1072
|
+
return
|
|
1073
|
+
seen.add(key)
|
|
1074
|
+
related.append(
|
|
1075
|
+
{
|
|
1076
|
+
"object": obj_name,
|
|
1077
|
+
"member": str(member),
|
|
1078
|
+
"member_type": member_type,
|
|
1079
|
+
"reason": reason,
|
|
1080
|
+
}
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
resolved = _resolve_interface(index, name)
|
|
1084
|
+
if resolved is not None:
|
|
1085
|
+
obj_name, obj_data = resolved
|
|
1086
|
+
for m in obj_data.get("methods", []):
|
|
1087
|
+
_add(obj_name, m, "method", "same interface")
|
|
1088
|
+
for p in obj_data.get("properties", []):
|
|
1089
|
+
_add(obj_name, p, "property", "same interface")
|
|
1090
|
+
return {
|
|
1091
|
+
"found": True,
|
|
1092
|
+
"query": name,
|
|
1093
|
+
"resolved_as": "interface",
|
|
1094
|
+
"related": related[:max_results],
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
lowered = name.lower()
|
|
1098
|
+
matched_member = False
|
|
1099
|
+
for obj_name, obj_data in com_objects.items():
|
|
1100
|
+
members = [(m, "method") for m in obj_data.get("methods", [])] + [
|
|
1101
|
+
(p, "property") for p in obj_data.get("properties", [])
|
|
1102
|
+
]
|
|
1103
|
+
names_lower = {str(m).lower() for m, _ in members}
|
|
1104
|
+
if lowered in names_lower:
|
|
1105
|
+
matched_member = True
|
|
1106
|
+
for member, member_type in members:
|
|
1107
|
+
_add(obj_name, member, member_type, f"sibling on {obj_name}")
|
|
1108
|
+
else:
|
|
1109
|
+
for member, member_type in members:
|
|
1110
|
+
if lowered in str(member).lower():
|
|
1111
|
+
_add(obj_name, member, member_type, "name match")
|
|
1112
|
+
|
|
1113
|
+
return {
|
|
1114
|
+
"found": matched_member or bool(related),
|
|
1115
|
+
"query": name,
|
|
1116
|
+
"resolved_as": "member" if matched_member else "name-match",
|
|
1117
|
+
"related": related[:max_results],
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def _fallback_help_for_query(query: str) -> dict[str, Any]:
|
|
1122
|
+
"""Provide coherent fallback help when no docs index is available.
|
|
1123
|
+
|
|
1124
|
+
Args:
|
|
1125
|
+
query (str): Query text used for the operation.
|
|
1126
|
+
|
|
1127
|
+
Returns:
|
|
1128
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
1129
|
+
"""
|
|
1130
|
+
q = query.lower()
|
|
1131
|
+
|
|
1132
|
+
if any(word in q for word in ("revolve", "lathe", "turned")):
|
|
1133
|
+
return {
|
|
1134
|
+
"suggested_tools": [
|
|
1135
|
+
"create_part",
|
|
1136
|
+
"create_sketch",
|
|
1137
|
+
"add_centerline",
|
|
1138
|
+
"add_line",
|
|
1139
|
+
"add_arc",
|
|
1140
|
+
"exit_sketch",
|
|
1141
|
+
"create_revolve",
|
|
1142
|
+
],
|
|
1143
|
+
"next_steps": [
|
|
1144
|
+
"Create a profile sketch and a centerline in the same sketch.",
|
|
1145
|
+
"Exit sketch mode before create_revolve.",
|
|
1146
|
+
"If real COM returns parameter mismatch, use generate_vba_revolve as fallback guidance.",
|
|
1147
|
+
],
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if any(word in q for word in ("extrude", "boss", "cut")):
|
|
1151
|
+
return {
|
|
1152
|
+
"suggested_tools": [
|
|
1153
|
+
"create_part",
|
|
1154
|
+
"create_sketch",
|
|
1155
|
+
"add_rectangle",
|
|
1156
|
+
"add_circle",
|
|
1157
|
+
"exit_sketch",
|
|
1158
|
+
"create_extrusion",
|
|
1159
|
+
],
|
|
1160
|
+
"next_steps": [
|
|
1161
|
+
"Ensure sketch profile is closed before create_extrusion.",
|
|
1162
|
+
"Use direction='cut' for removal operations.",
|
|
1163
|
+
],
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
return {
|
|
1167
|
+
"suggested_tools": [
|
|
1168
|
+
"discover_solidworks_docs",
|
|
1169
|
+
"open_model",
|
|
1170
|
+
"get_file_properties",
|
|
1171
|
+
"save_as",
|
|
1172
|
+
],
|
|
1173
|
+
"next_steps": [
|
|
1174
|
+
"Run discover_solidworks_docs to index the local COM API surface.",
|
|
1175
|
+
"Search again with a narrower API term like a method or interface name.",
|
|
1176
|
+
],
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
async def register_docs_discovery_tools(
|
|
1181
|
+
mcp: Any, adapter: Any, config: dict[str, Any]
|
|
1182
|
+
) -> int:
|
|
1183
|
+
"""Register docs discovery tool with FastMCP.
|
|
1184
|
+
|
|
1185
|
+
Args:
|
|
1186
|
+
mcp (Any): The mcp value.
|
|
1187
|
+
adapter (Any): Adapter instance used for the operation.
|
|
1188
|
+
config (dict[str, Any]): Configuration values for the operation.
|
|
1189
|
+
|
|
1190
|
+
Returns:
|
|
1191
|
+
int: The computed numeric result.
|
|
1192
|
+
"""
|
|
1193
|
+
mcp = with_tool_boundary(mcp)
|
|
1194
|
+
|
|
1195
|
+
@mcp.tool() # type: ignore[untyped-decorator]
|
|
1196
|
+
async def discover_solidworks_docs(
|
|
1197
|
+
input_data: DiscoverDocsInput | None = None,
|
|
1198
|
+
) -> dict[str, Any]:
|
|
1199
|
+
"""Discover and index SolidWorks COM and VBA documentation.
|
|
1200
|
+
|
|
1201
|
+
Creates a searchable index of all available COM objects, methods, properties, and VBA
|
|
1202
|
+
libraries for the installed SolidWorks version. Useful for building context for
|
|
1203
|
+
intelligent MCP tool selection and documentation queries.
|
|
1204
|
+
|
|
1205
|
+
Args:
|
|
1206
|
+
input_data (DiscoverDocsInput | None): The input data value. Defaults to None.
|
|
1207
|
+
|
|
1208
|
+
Returns:
|
|
1209
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
1210
|
+
|
|
1211
|
+
Example:
|
|
1212
|
+
```python
|
|
1213
|
+
result = await discover_solidworks_docs({
|
|
1214
|
+
"output_dir": "docs-index",
|
|
1215
|
+
"include_vba": True
|
|
1216
|
+
})
|
|
1217
|
+
|
|
1218
|
+
if result["status"] == "success":
|
|
1219
|
+
print(f"Discovered {result['summary']['total_methods']} COM methods")
|
|
1220
|
+
print(f"Output saved to {result['output_file']}")
|
|
1221
|
+
```
|
|
1222
|
+
|
|
1223
|
+
Note:
|
|
1224
|
+
- Requires Windows + SolidWorks installed and running
|
|
1225
|
+
- Creates .generated/docs-index directory if not present
|
|
1226
|
+
- Generates solidworks_docs_index.json with full catalog
|
|
1227
|
+
- Index can be used to build context for RAG or tool discovery
|
|
1228
|
+
"""
|
|
1229
|
+
import time
|
|
1230
|
+
|
|
1231
|
+
start_time = time.time()
|
|
1232
|
+
|
|
1233
|
+
if not HAS_WIN32COM:
|
|
1234
|
+
raise UnsupportedOperationError(
|
|
1235
|
+
"win32com not available; cannot discover COM documentation"
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
if platform.system() != "Windows":
|
|
1239
|
+
raise UnsupportedOperationError("COM discovery only available on Windows")
|
|
1240
|
+
|
|
1241
|
+
normalized = normalize_input(input_data, DiscoverDocsInput)
|
|
1242
|
+
output_dir = Path(normalized.output_dir) if normalized.output_dir else None
|
|
1243
|
+
year = _resolve_solidworks_year(normalized.year, config)
|
|
1244
|
+
|
|
1245
|
+
discovery = SolidWorksDocsDiscovery(output_dir=output_dir)
|
|
1246
|
+
|
|
1247
|
+
logger.info("Starting SolidWorks documentation discovery...")
|
|
1248
|
+
index = discovery.discover_all()
|
|
1249
|
+
if discovery.sw_app is None:
|
|
1250
|
+
raise ToolOperationError(
|
|
1251
|
+
"SolidWorks is not running in this session; start SolidWorks "
|
|
1252
|
+
"and retry. Docs discovery attaches to a running instance and "
|
|
1253
|
+
"never launches one."
|
|
1254
|
+
)
|
|
1255
|
+
|
|
1256
|
+
filename = (
|
|
1257
|
+
f"solidworks_docs_index_{year}.json"
|
|
1258
|
+
if year
|
|
1259
|
+
else "solidworks_docs_index.json"
|
|
1260
|
+
)
|
|
1261
|
+
output_file = discovery.save_index(filename=filename)
|
|
1262
|
+
|
|
1263
|
+
# Rebuild the FAISS RAG namespace so Gemma can query the API surface.
|
|
1264
|
+
rag_indexed = False
|
|
1265
|
+
if output_file is not None:
|
|
1266
|
+
try:
|
|
1267
|
+
from solidworks_mcp.agents.vector_rag import (
|
|
1268
|
+
build_solidworks_api_docs_index,
|
|
1269
|
+
)
|
|
1270
|
+
|
|
1271
|
+
rag_idx = build_solidworks_api_docs_index(output_file)
|
|
1272
|
+
rag_idx.save()
|
|
1273
|
+
rag_indexed = True
|
|
1274
|
+
logger.info(
|
|
1275
|
+
"FAISS 'solidworks-api-docs' namespace rebuilt: %d chunks",
|
|
1276
|
+
rag_idx.chunk_count,
|
|
1277
|
+
)
|
|
1278
|
+
except ImportError:
|
|
1279
|
+
logger.debug("faiss-cpu not installed; skipping RAG index rebuild")
|
|
1280
|
+
except Exception as _rag_exc:
|
|
1281
|
+
logger.warning("RAG rebuild failed: {}", _rag_exc)
|
|
1282
|
+
|
|
1283
|
+
summary = discovery.create_search_summary()
|
|
1284
|
+
|
|
1285
|
+
return {
|
|
1286
|
+
"status": "success",
|
|
1287
|
+
"message": f"Discovered {summary['total_com_objects']} COM objects, "
|
|
1288
|
+
f"{summary['total_methods']} methods, "
|
|
1289
|
+
f"{summary['total_properties']} properties",
|
|
1290
|
+
"index": index,
|
|
1291
|
+
"summary": summary,
|
|
1292
|
+
"year": year,
|
|
1293
|
+
"output_file": str(output_file) if output_file else None,
|
|
1294
|
+
"rag_indexed": rag_indexed,
|
|
1295
|
+
"execution_time": time.time() - start_time,
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
@mcp.tool() # type: ignore[untyped-decorator]
|
|
1299
|
+
async def search_solidworks_api_help(
|
|
1300
|
+
input_data: SearchApiHelpInput | None = None,
|
|
1301
|
+
) -> dict[str, Any]:
|
|
1302
|
+
"""Search SolidWorks API help index and return coherent guidance.
|
|
1303
|
+
|
|
1304
|
+
This tool helps when the LLM gets stuck by mapping user intent to discovered COM members
|
|
1305
|
+
and practical MCP workflow guidance.
|
|
1306
|
+
|
|
1307
|
+
Args:
|
|
1308
|
+
input_data (SearchApiHelpInput | None): The input data value. Defaults to None.
|
|
1309
|
+
|
|
1310
|
+
Returns:
|
|
1311
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
1312
|
+
"""
|
|
1313
|
+
|
|
1314
|
+
import time
|
|
1315
|
+
|
|
1316
|
+
start_time = time.time()
|
|
1317
|
+
|
|
1318
|
+
normalized = normalize_input(input_data, SearchApiHelpInput)
|
|
1319
|
+
year = _resolve_solidworks_year(normalized.year, config)
|
|
1320
|
+
|
|
1321
|
+
index_file = _find_index_file(year, normalized.index_file)
|
|
1322
|
+
index = _load_index_file(index_file) if index_file else None
|
|
1323
|
+
|
|
1324
|
+
# Rebuild when stale (config-gated), or when missing and the caller
|
|
1325
|
+
# opted in.
|
|
1326
|
+
index, index_file = _maybe_refresh_index(index, index_file, year, config)
|
|
1327
|
+
if index is None and normalized.auto_discover_if_missing:
|
|
1328
|
+
if HAS_WIN32COM and platform.system() == "Windows":
|
|
1329
|
+
discovery = SolidWorksDocsDiscovery()
|
|
1330
|
+
discovered = discovery.discover_all()
|
|
1331
|
+
if discovery.sw_app is not None:
|
|
1332
|
+
index = discovered
|
|
1333
|
+
filename = (
|
|
1334
|
+
f"solidworks_docs_index_{year}.json"
|
|
1335
|
+
if year
|
|
1336
|
+
else "solidworks_docs_index.json"
|
|
1337
|
+
)
|
|
1338
|
+
index_file = discovery.save_index(filename=filename)
|
|
1339
|
+
|
|
1340
|
+
matches = _search_index(index or {}, normalized.query, normalized.max_results)
|
|
1341
|
+
fallback = _fallback_help_for_query(normalized.query)
|
|
1342
|
+
|
|
1343
|
+
guidance_lines = []
|
|
1344
|
+
if matches:
|
|
1345
|
+
guidance_lines.append(
|
|
1346
|
+
f"Found {len(matches)} API matches for '{normalized.query}'."
|
|
1347
|
+
)
|
|
1348
|
+
guidance_lines.append(
|
|
1349
|
+
"Start with the highest-score members and verify with the real adapter path."
|
|
1350
|
+
)
|
|
1351
|
+
else:
|
|
1352
|
+
guidance_lines.append(
|
|
1353
|
+
"No indexed API hits found; using workflow fallback guidance."
|
|
1354
|
+
)
|
|
1355
|
+
guidance_lines.append(
|
|
1356
|
+
"Consider running discover_solidworks_docs first for this machine/version."
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
return {
|
|
1360
|
+
"status": "success",
|
|
1361
|
+
"message": "SolidWorks API help search completed",
|
|
1362
|
+
"query": normalized.query,
|
|
1363
|
+
"year": year,
|
|
1364
|
+
"source_index_file": str(index_file) if index_file else None,
|
|
1365
|
+
"matches": matches,
|
|
1366
|
+
"fallback_help": fallback,
|
|
1367
|
+
"guidance": " ".join(guidance_lines),
|
|
1368
|
+
"execution_time": time.time() - start_time,
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
def _load_index_for_lookup(
|
|
1372
|
+
requested_year: int | None, explicit_index_file: str | None
|
|
1373
|
+
) -> tuple[dict[str, Any] | None, Path | None, int | None]:
|
|
1374
|
+
"""Shared index resolution for the granular lookup tools."""
|
|
1375
|
+
resolved_year = _resolve_solidworks_year(requested_year, config)
|
|
1376
|
+
index_file = _find_index_file(resolved_year, explicit_index_file)
|
|
1377
|
+
index = _load_index_file(index_file) if index_file else None
|
|
1378
|
+
index, index_file = _maybe_refresh_index(
|
|
1379
|
+
index, index_file, resolved_year, config
|
|
1380
|
+
)
|
|
1381
|
+
return index, index_file, resolved_year
|
|
1382
|
+
|
|
1383
|
+
@mcp.tool() # type: ignore[untyped-decorator]
|
|
1384
|
+
async def lookup_api_method(
|
|
1385
|
+
input_data: LookupApiMethodInput | None = None,
|
|
1386
|
+
) -> dict[str, Any]:
|
|
1387
|
+
"""Look up one SolidWorks API method or property by interface + name.
|
|
1388
|
+
|
|
1389
|
+
Confirms the member is indexed on that interface and returns its
|
|
1390
|
+
sibling members for context. The local index stores names only, so
|
|
1391
|
+
full signatures (parameters, return type) are not available here - use
|
|
1392
|
+
search_solidworks_api_help or help.solidworks.com for those.
|
|
1393
|
+
"""
|
|
1394
|
+
import time
|
|
1395
|
+
|
|
1396
|
+
start = time.time()
|
|
1397
|
+
normalized = normalize_input(input_data, LookupApiMethodInput)
|
|
1398
|
+
index, index_file, year = _load_index_for_lookup(
|
|
1399
|
+
normalized.year, normalized.index_file
|
|
1400
|
+
)
|
|
1401
|
+
result = _lookup_method(index or {}, normalized.interface, normalized.method)
|
|
1402
|
+
return {
|
|
1403
|
+
"status": "success",
|
|
1404
|
+
"message": (
|
|
1405
|
+
"Method found"
|
|
1406
|
+
if result.get("found")
|
|
1407
|
+
else "No indexed match for that interface/method"
|
|
1408
|
+
),
|
|
1409
|
+
"year": year,
|
|
1410
|
+
"source_index_file": str(index_file) if index_file else None,
|
|
1411
|
+
"result": result,
|
|
1412
|
+
"execution_time": time.time() - start,
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
@mcp.tool() # type: ignore[untyped-decorator]
|
|
1416
|
+
async def lookup_api_interface(
|
|
1417
|
+
input_data: LookupApiInterfaceInput | None = None,
|
|
1418
|
+
) -> dict[str, Any]:
|
|
1419
|
+
"""List every indexed member (methods + properties) of a COM interface."""
|
|
1420
|
+
import time
|
|
1421
|
+
|
|
1422
|
+
start = time.time()
|
|
1423
|
+
normalized = normalize_input(input_data, LookupApiInterfaceInput)
|
|
1424
|
+
index, index_file, year = _load_index_for_lookup(
|
|
1425
|
+
normalized.year, normalized.index_file
|
|
1426
|
+
)
|
|
1427
|
+
result = _lookup_interface(index or {}, normalized.interface)
|
|
1428
|
+
return {
|
|
1429
|
+
"status": "success",
|
|
1430
|
+
"message": (
|
|
1431
|
+
f"{result['method_count']} methods, {result['property_count']} properties"
|
|
1432
|
+
if result.get("found")
|
|
1433
|
+
else "Interface not found in the indexed documentation"
|
|
1434
|
+
),
|
|
1435
|
+
"year": year,
|
|
1436
|
+
"source_index_file": str(index_file) if index_file else None,
|
|
1437
|
+
"result": result,
|
|
1438
|
+
"execution_time": time.time() - start,
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
@mcp.tool() # type: ignore[untyped-decorator]
|
|
1442
|
+
async def find_related_api_members(
|
|
1443
|
+
input_data: FindRelatedApiInput | None = None,
|
|
1444
|
+
) -> dict[str, Any]:
|
|
1445
|
+
"""Find API members related to a known interface or member name.
|
|
1446
|
+
|
|
1447
|
+
For an interface: its own members. For a member: the other members of
|
|
1448
|
+
every interface that also exposes it, plus name-substring matches.
|
|
1449
|
+
"""
|
|
1450
|
+
import time
|
|
1451
|
+
|
|
1452
|
+
start = time.time()
|
|
1453
|
+
normalized = normalize_input(input_data, FindRelatedApiInput)
|
|
1454
|
+
index, index_file, year = _load_index_for_lookup(
|
|
1455
|
+
normalized.year, normalized.index_file
|
|
1456
|
+
)
|
|
1457
|
+
result = _find_related_api_members(
|
|
1458
|
+
index or {}, normalized.name, normalized.max_results
|
|
1459
|
+
)
|
|
1460
|
+
return {
|
|
1461
|
+
"status": "success",
|
|
1462
|
+
"message": (
|
|
1463
|
+
f"{len(result['related'])} related member(s)"
|
|
1464
|
+
if result.get("found")
|
|
1465
|
+
else "No related members found"
|
|
1466
|
+
),
|
|
1467
|
+
"year": year,
|
|
1468
|
+
"source_index_file": str(index_file) if index_file else None,
|
|
1469
|
+
"result": result,
|
|
1470
|
+
"execution_time": time.time() - start,
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
# discover_solidworks_docs, search_solidworks_api_help, lookup_api_method,
|
|
1474
|
+
# lookup_api_interface, find_related_api_members
|
|
1475
|
+
tool_count = 5
|
|
1476
|
+
return tool_count
|