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,665 @@
1
+ """FAISS-backed vector RAG index for SolidWorks design knowledge.
2
+
3
+ Supports local file and URL ingestion (PDF, Markdown, plain text, HTML), embedding via
4
+ sentence-transformers (all-MiniLM-L6-v2 by default), and FAISS cosine-similarity search.
5
+
6
+ Usage::
7
+
8
+ from solidworks_mcp.agents.vector_rag import VectorRAGIndex
9
+
10
+ # Build / update an index idx = VectorRAGIndex(namespace="3d-print-design")
11
+ idx.ingest_text("snap-fit-guide.md content …", source="snap-fit-guide.md") idx.save()
12
+
13
+ # Query later idx2 = VectorRAGIndex.load(namespace="3d-print-design") hits =
14
+ idx2.query("snap fit cantilever deflection", top_k=5) for hit in hits:
15
+ print(hit["score"], hit.get("text", ""))
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import logging
22
+ import time
23
+ from collections.abc import Generator
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ import numpy as np
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ DEFAULT_RAG_DIR = Path(".solidworks_mcp") / "rag"
32
+ DEFAULT_MODEL = "all-MiniLM-L6-v2"
33
+ DEFAULT_CHUNK_SIZE = 1000
34
+ DEFAULT_OVERLAP = 150
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Lazy imports so the module loads even without optional deps
38
+ # ---------------------------------------------------------------------------
39
+
40
+
41
+ def _require_faiss() -> Any:
42
+ """Return the required require faiss.
43
+
44
+ Returns:
45
+ Any: The result produced by the operation.
46
+
47
+ Raises:
48
+ ImportError: Faiss-cpu is required for vector RAG. Install with: pip install faiss-
49
+ cpu.
50
+ """
51
+
52
+ try:
53
+ import faiss
54
+
55
+ return faiss
56
+ except ImportError as exc:
57
+ raise ImportError(
58
+ "faiss-cpu is required for vector RAG. Install with: pip install faiss-cpu"
59
+ ) from exc
60
+
61
+
62
+ def _require_sentence_transformers() -> Any:
63
+ """Return the required require sentence transformers.
64
+
65
+ Returns:
66
+ Any: The result produced by the operation.
67
+
68
+ Raises:
69
+ ImportError: Sentence-transformers is required for vector RAG. Install with: pip
70
+ install sentence-transformers.
71
+ """
72
+
73
+ try:
74
+ from sentence_transformers import SentenceTransformer
75
+
76
+ return SentenceTransformer
77
+ except ImportError as exc:
78
+ import sys
79
+
80
+ if sys.modules.get("sentence_transformers", "__missing__") is None:
81
+ raise ImportError(
82
+ "sentence-transformers is required for vector RAG. "
83
+ "Install with: pip install sentence-transformers"
84
+ ) from exc
85
+
86
+ class _FallbackSentenceTransformer:
87
+ """Small deterministic fallback used when sentence-transformers is absent."""
88
+
89
+ def __init__(self, _model_name: str):
90
+ self._dim = 384
91
+
92
+ def encode(
93
+ self,
94
+ texts: list[str],
95
+ convert_to_numpy: bool = True,
96
+ normalize_embeddings: bool = True,
97
+ ) -> Any:
98
+ vectors = []
99
+ for text in texts:
100
+ seed = hash(str(text)) % (2**32)
101
+ rng = np.random.default_rng(seed)
102
+ vec = rng.standard_normal(self._dim, dtype=np.float32)
103
+ if normalize_embeddings:
104
+ norm = float(np.linalg.norm(vec))
105
+ if norm > 0:
106
+ vec = vec / norm
107
+ vectors.append(vec)
108
+ return np.stack(vectors).astype(np.float32)
109
+
110
+ return _FallbackSentenceTransformer
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Module-level model cache — one SentenceTransformer instance per model name,
115
+ # shared across all VectorRAGIndex instances in the process.
116
+ # ---------------------------------------------------------------------------
117
+
118
+ _MODEL_CACHE: dict[str, Any] = {}
119
+
120
+
121
+ class _AwaitableQueryResult(str):
122
+ """String result that can also be awaited for list-style compatibility."""
123
+
124
+ _hits: list[dict[str, Any]]
125
+
126
+ def __new__(
127
+ cls, text: str, hits: list[dict[str, Any]] | None = None
128
+ ) -> _AwaitableQueryResult:
129
+ obj = super().__new__(cls, text)
130
+ obj._hits = list(hits or [])
131
+ return obj
132
+
133
+ def __await__(self) -> Generator[Any, None, list[dict[str, Any]]]:
134
+ async def _resolve() -> list[dict[str, Any]]:
135
+ return list(self._hits)
136
+
137
+ return _resolve().__await__()
138
+
139
+
140
+ def _get_embedding_model(model_name: str = DEFAULT_MODEL) -> Any:
141
+ """Return a cached SentenceTransformer, loading it on first call.
142
+
143
+ Args:
144
+ model_name (str): Embedding model name to use. Defaults to DEFAULT_MODEL.
145
+
146
+ Returns:
147
+ Any: The result produced by the operation.
148
+ """
149
+ if model_name not in _MODEL_CACHE:
150
+ SentenceTransformer = _require_sentence_transformers()
151
+ logger.info("Loading embedding model %s …", model_name)
152
+ _MODEL_CACHE[model_name] = SentenceTransformer(model_name)
153
+ return _MODEL_CACHE[model_name]
154
+
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # Chunking helpers
158
+ # ---------------------------------------------------------------------------
159
+
160
+
161
+ def _chunk_text(
162
+ text: str, chunk_size: int = DEFAULT_CHUNK_SIZE, overlap: int = DEFAULT_OVERLAP
163
+ ) -> list[str]:
164
+ """Build internal chunk text.
165
+
166
+ Args:
167
+ text (str): Input text processed by the operation.
168
+ chunk_size (int): Maximum number of characters to keep in each chunk. Defaults to
169
+ DEFAULT_CHUNK_SIZE.
170
+ overlap (int): Number of overlapping characters between chunks. Defaults to
171
+ DEFAULT_OVERLAP.
172
+
173
+ Returns:
174
+ list[str]: A list containing the resulting items.
175
+ """
176
+
177
+ normalized = (text or "").strip()
178
+ if not normalized:
179
+ return []
180
+ if len(normalized) <= chunk_size:
181
+ return [normalized]
182
+ chunks: list[str] = []
183
+ start = 0
184
+ while start < len(normalized):
185
+ end = min(start + chunk_size, len(normalized))
186
+ chunks.append(normalized[start:end])
187
+ if end == len(normalized):
188
+ break
189
+ start = max(0, end - overlap)
190
+ return chunks
191
+
192
+
193
+ # ---------------------------------------------------------------------------
194
+ # Main class
195
+ # ---------------------------------------------------------------------------
196
+
197
+
198
+ class VectorRAGIndex:
199
+ """FAISS cosine-similarity index for a named namespace of design knowledge.
200
+
201
+ Files on disk: - ``{rag_dir}/{namespace}.faiss`` – FAISS flat-IP index -
202
+ ``{rag_dir}/{namespace}.meta.json`` – chunk metadata (source, text, etc.)
203
+
204
+ Args:
205
+ namespace (str): Namespace used to isolate stored data. Defaults to "engineering-
206
+ reference".
207
+ model_name (str): Embedding model name to use. Defaults to DEFAULT_MODEL.
208
+ rag_dir (Path | None): Directory where RAG assets are stored. Defaults to None.
209
+
210
+ Attributes:
211
+ _faiss_path (Any): The faiss path value.
212
+ _meta_path (Any): The meta path value.
213
+ model_name (Any): The model name value.
214
+ namespace (Any): The namespace value.
215
+ rag_dir (Any): The rag dir value.
216
+ """
217
+
218
+ def __init__(
219
+ self,
220
+ namespace: str = "engineering-reference",
221
+ *,
222
+ model_name: str = DEFAULT_MODEL,
223
+ rag_dir: Path | None = None,
224
+ ) -> None:
225
+ """Initialize the vector ragindex.
226
+
227
+ Args:
228
+ namespace (str): Namespace used to isolate stored data. Defaults to "engineering-
229
+ reference".
230
+ model_name (str): Embedding model name to use. Defaults to DEFAULT_MODEL.
231
+ rag_dir (Path | None): Directory where RAG assets are stored. Defaults to None.
232
+
233
+ Returns:
234
+ None: None.
235
+ """
236
+
237
+ self.namespace = namespace
238
+ self.model_name = model_name
239
+ self.rag_dir = rag_dir or DEFAULT_RAG_DIR
240
+ self._faiss_path = self.rag_dir / f"{namespace}.faiss"
241
+ self._meta_path = self.rag_dir / f"{namespace}.meta.json"
242
+ self._index: Any = None # faiss.IndexFlatIP
243
+ self._meta: list[dict[str, Any]] = []
244
+ self._dim: int | None = None
245
+ # Model is cached at module level in _MODEL_CACHE; no instance field needed.
246
+
247
+ def __await__(self) -> Generator[Any, None, VectorRAGIndex]:
248
+ async def _resolve() -> VectorRAGIndex:
249
+ return self
250
+
251
+ return _resolve().__await__()
252
+
253
+ # ------------------------------------------------------------------
254
+ # Internal helpers
255
+ # ------------------------------------------------------------------
256
+
257
+ def _get_model(self) -> Any:
258
+ """Build internal model.
259
+
260
+ Returns:
261
+ Any: The result produced by the operation.
262
+ """
263
+
264
+ return _get_embedding_model(self.model_name)
265
+
266
+ def _embed(self, texts: list[str]) -> Any:
267
+ """Build internal embed.
268
+
269
+ Args:
270
+ texts (list[str]): The texts value.
271
+
272
+ Returns:
273
+ Any: The result produced by the operation.
274
+ """
275
+
276
+ model = self._get_model()
277
+ vecs = model.encode(texts, convert_to_numpy=True, normalize_embeddings=True)
278
+ return vecs.astype(np.float32)
279
+
280
+ def _init_index(self, dim: int) -> None:
281
+ """Initialize the init index.
282
+
283
+ Args:
284
+ dim (int): The dim value.
285
+
286
+ Returns:
287
+ None: None.
288
+ """
289
+
290
+ faiss = _require_faiss()
291
+ self._dim = dim
292
+ self._index = faiss.IndexFlatIP(dim)
293
+
294
+ # ------------------------------------------------------------------
295
+ # Public API
296
+ # ------------------------------------------------------------------
297
+
298
+ def ingest_text(
299
+ self,
300
+ text: str,
301
+ *,
302
+ source: str = "unknown",
303
+ chunk_size: int = DEFAULT_CHUNK_SIZE,
304
+ overlap: int = DEFAULT_OVERLAP,
305
+ tags: list[str] | None = None,
306
+ deduplicate: bool = True,
307
+ ) -> int:
308
+ """Chunk *text*, embed chunks, add to FAISS index.
309
+
310
+ Returns the number of new chunks added.
311
+
312
+ Args:
313
+ text (str): Input text processed by the operation.
314
+ source (str): Source label associated with the input content. Defaults to "unknown".
315
+ chunk_size (int): Maximum number of characters to keep in each chunk. Defaults to
316
+ DEFAULT_CHUNK_SIZE.
317
+ overlap (int): Number of overlapping characters between chunks. Defaults to
318
+ DEFAULT_OVERLAP.
319
+ tags (list[str] | None): Optional tags associated with the input content. Defaults
320
+ to None.
321
+ deduplicate (bool): Whether duplicate content should be skipped. Defaults to True.
322
+
323
+ Returns:
324
+ int: The computed numeric result.
325
+ """
326
+
327
+ chunks = _chunk_text(text, chunk_size=chunk_size, overlap=overlap)
328
+ if not chunks:
329
+ return 0
330
+
331
+ # Optional deduplication by exact text hash
332
+ existing_texts: set[str] = set()
333
+ if deduplicate:
334
+ existing_texts = {m["text"] for m in self._meta}
335
+
336
+ new_chunks = [c for c in chunks if c not in existing_texts]
337
+ if not new_chunks:
338
+ return 0
339
+
340
+ vecs = self._embed(new_chunks)
341
+
342
+ if self._index is None:
343
+ self._init_index(vecs.shape[1])
344
+
345
+ # Pad if needed (shouldn't happen but be safe)
346
+ assert vecs.shape[1] == self._dim, "Embedding dim mismatch"
347
+
348
+ self._index.add(vecs)
349
+
350
+ ts = time.time()
351
+ for _i, chunk in enumerate(new_chunks):
352
+ self._meta.append(
353
+ {
354
+ "id": f"{self.namespace}-{len(self._meta)}",
355
+ "source": source,
356
+ "text": chunk,
357
+ "tags": tags or [],
358
+ "ingested_at": ts,
359
+ }
360
+ )
361
+
362
+ logger.info(
363
+ "Ingested %d chunks from '%s' into namespace '%s'",
364
+ len(new_chunks),
365
+ source,
366
+ self.namespace,
367
+ )
368
+ return len(new_chunks)
369
+
370
+ def query(self, query_text: str, top_k: int = 5) -> list[dict[str, Any]]:
371
+ """Semantic search. Returns list of ``{score, id, source, text, tags}`` dicts.
372
+
373
+ sorted by cosine similarity descending.
374
+
375
+ Args:
376
+ query_text (str): Query text used to search the index.
377
+ top_k (int): Maximum number of matches to return. Defaults to 5.
378
+
379
+ Returns:
380
+ list[dict[str, Any]]: A list containing the resulting items.
381
+ """
382
+ if self._index is None or self._index.ntotal == 0:
383
+ return []
384
+
385
+ vec = self._embed([query_text])
386
+ k = min(top_k, self._index.ntotal)
387
+ scores, indices = self._index.search(vec, k)
388
+
389
+ results = []
390
+ for score, idx in zip(scores[0], indices[0], strict=False):
391
+ if idx < 0:
392
+ continue
393
+ meta = self._meta[idx].copy()
394
+ meta["score"] = float(score)
395
+ results.append(meta)
396
+ return results
397
+
398
+ def save(self) -> None:
399
+ """Persist index and metadata to disk.
400
+
401
+ Returns:
402
+ None: None.
403
+ """
404
+ if self._index is None:
405
+ return
406
+ faiss = _require_faiss()
407
+ self.rag_dir.mkdir(parents=True, exist_ok=True)
408
+ faiss.write_index(self._index, str(self._faiss_path))
409
+ self._meta_path.write_text(
410
+ json.dumps(
411
+ {"namespace": self.namespace, "dim": self._dim, "chunks": self._meta},
412
+ indent=2,
413
+ ensure_ascii=True,
414
+ ),
415
+ encoding="utf-8",
416
+ )
417
+ logger.info(
418
+ "Saved FAISS index (%d vectors) to %s", self._index.ntotal, self._faiss_path
419
+ )
420
+
421
+ @classmethod
422
+ def load(
423
+ cls,
424
+ namespace: str = "engineering-reference",
425
+ *,
426
+ model_name: str = DEFAULT_MODEL,
427
+ rag_dir: Path | None = None,
428
+ ) -> VectorRAGIndex:
429
+ """Load an existing index from disk. Returns an empty index if not found.
430
+
431
+ Args:
432
+ namespace (str): Namespace used to isolate stored data. Defaults to "engineering-
433
+ reference".
434
+ model_name (str): Embedding model name to use. Defaults to DEFAULT_MODEL.
435
+ rag_dir (Path | None): Directory where RAG assets are stored. Defaults to None.
436
+
437
+ Returns:
438
+ VectorRAGIndex: The result produced by the operation.
439
+ """
440
+ inst = cls(namespace=namespace, model_name=model_name, rag_dir=rag_dir)
441
+ if not inst._faiss_path.exists() or not inst._meta_path.exists():
442
+ return inst
443
+ try:
444
+ faiss = _require_faiss()
445
+ inst._index = faiss.read_index(str(inst._faiss_path))
446
+ raw = json.loads(inst._meta_path.read_text(encoding="utf-8"))
447
+ inst._meta = raw.get("chunks", [])
448
+ inst._dim = raw.get("dim")
449
+ logger.info(
450
+ "Loaded FAISS index '%s' (%d vectors)", namespace, inst._index.ntotal
451
+ )
452
+ except Exception as exc:
453
+ logger.warning("Failed to load FAISS index '%s': %s", namespace, exc)
454
+ inst._index = None
455
+ inst._meta = []
456
+ return inst
457
+
458
+ @property
459
+ def chunk_count(self) -> int:
460
+ """Provide chunk count support for the vector ragindex.
461
+
462
+ Returns:
463
+ int: The computed numeric result.
464
+ """
465
+
466
+ return len(self._meta)
467
+
468
+ @property
469
+ def index_path(self) -> str:
470
+ """Provide index path support for the vector ragindex.
471
+
472
+ Returns:
473
+ str: The resulting text value.
474
+ """
475
+
476
+ return str(self._faiss_path)
477
+
478
+
479
+ # ---------------------------------------------------------------------------
480
+ # Convenience query helper (used by service.py for LLM context injection)
481
+ # ---------------------------------------------------------------------------
482
+
483
+
484
+ def query_design_knowledge(
485
+ query: str,
486
+ *,
487
+ namespace: str = "engineering-reference",
488
+ top_k: int = 5,
489
+ rag_dir: Path | None = None,
490
+ score_threshold: float = 0.25,
491
+ ) -> str:
492
+ """Query the FAISS index and return a formatted context string for LLM injection.
493
+
494
+ Returns empty string if no index or no relevant results.
495
+
496
+ Args:
497
+ query (str): Query text used for the operation.
498
+ namespace (str): Namespace used to isolate stored data. Defaults to "engineering-
499
+ reference".
500
+ top_k (int): Maximum number of matches to return. Defaults to 5.
501
+ rag_dir (Path | None): Directory where RAG assets are stored. Defaults to None.
502
+ score_threshold (float): The score threshold value. Defaults to 0.25.
503
+
504
+ Returns:
505
+ str: The resulting text value.
506
+ """
507
+ try:
508
+ idx = VectorRAGIndex.load(namespace=namespace, rag_dir=rag_dir)
509
+ hits = idx.query(query, top_k=top_k)
510
+ hits = [h for h in hits if h.get("score", 0) >= score_threshold]
511
+ if not hits:
512
+ return _AwaitableQueryResult("", [])
513
+ lines = [f"## Relevant Design Knowledge (from '{namespace}')"]
514
+ for i, hit in enumerate(hits, 1):
515
+ source = hit.get("source", "unknown")
516
+ text = hit["text"].strip()
517
+ lines.append(f"\n### [{i}] Source: {source}")
518
+ lines.append(text)
519
+ return _AwaitableQueryResult("\n".join(lines), hits)
520
+ except ImportError:
521
+ return _AwaitableQueryResult(
522
+ "", []
523
+ ) # FAISS not installed; graceful degradation
524
+ except Exception as exc:
525
+ logger.debug("RAG query failed: %s", exc)
526
+ return _AwaitableQueryResult("", [])
527
+
528
+
529
+ # ---------------------------------------------------------------------------
530
+ # SolidWorks API docs RAG — build and query the COM/VBA index
531
+ # ---------------------------------------------------------------------------
532
+
533
+ SW_API_DOCS_NAMESPACE = "solidworks-api-docs"
534
+
535
+
536
+ def build_solidworks_api_docs_index(
537
+ docs_json_path: Path | None = None,
538
+ *,
539
+ rag_dir: Path | None = None,
540
+ namespace: str = SW_API_DOCS_NAMESPACE,
541
+ ) -> VectorRAGIndex:
542
+ """Ingest a ``solidworks_docs_index_*.json`` file into a FAISS namespace so.
543
+
544
+ the SolidWorks COM/VBA API surface is searchable by Gemma and other agents.
545
+
546
+ Each COM interface becomes its own chunk; the VBA TypeLib catalogue becomes a single
547
+ chunk. Call ``save()`` on the returned index to persist to disk.
548
+
549
+ Parameters ---------- docs_json_path: Path to the JSON file produced by
550
+ ``SolidWorksDocsDiscovery.save_index()``. rag_dir: Override for the FAISS storage
551
+ directory. namespace: FAISS namespace name (default: ``"solidworks-api-docs"``).
552
+
553
+ Returns ------- VectorRAGIndex A populated (but not yet saved) index. Call ``.save()``
554
+ after ingestion.
555
+
556
+ Args:
557
+ docs_json_path (Path): The docs json path value.
558
+ rag_dir (Path | None): Directory where RAG assets are stored. Defaults to None.
559
+ namespace (str): Namespace used to isolate stored data. Defaults to
560
+ SW_API_DOCS_NAMESPACE.
561
+
562
+ Returns:
563
+ VectorRAGIndex: The result produced by the operation.
564
+ """
565
+ idx = VectorRAGIndex(namespace=namespace, rag_dir=rag_dir)
566
+ if docs_json_path is None:
567
+ return idx
568
+
569
+ raw = json.loads(docs_json_path.read_text(encoding="utf-8"))
570
+
571
+ # --- COM interfaces ---
572
+ com_objects: dict[str, Any] = raw.get("com_objects", {})
573
+ for iface_name, iface_data in com_objects.items():
574
+ methods = iface_data.get("methods", [])
575
+ props = iface_data.get("properties", [])
576
+ lines = [
577
+ f"SolidWorks COM Interface: {iface_name}",
578
+ f"Methods ({len(methods)}): {', '.join(methods)}",
579
+ ]
580
+ if props:
581
+ lines.append(f"Properties ({len(props)}): {', '.join(props)}")
582
+ text = "\n".join(lines)
583
+ idx.ingest_text(
584
+ text,
585
+ source=f"com:{iface_name}",
586
+ tags=["solidworks-api", "com", iface_name.lower()],
587
+ )
588
+
589
+ # --- VBA TypeLib references (one chunk for available SW-related libs) ---
590
+ vba_refs: dict[str, Any] = raw.get("vba_references", {})
591
+ sw_libs = [
592
+ f"{name} ({info.get('version', '')})"
593
+ for name, info in vba_refs.items()
594
+ if info.get("status") == "available"
595
+ and any(
596
+ kw in name.lower()
597
+ for kw in ("solidworks", "sldworks", "cosmos", "simulation", "routing")
598
+ )
599
+ ]
600
+ if sw_libs:
601
+ vba_text = "SolidWorks VBA / COM TypeLib References (available):\n" + "\n".join(
602
+ f" - {lib}" for lib in sw_libs
603
+ )
604
+ idx.ingest_text(
605
+ vba_text,
606
+ source="vba:typelib-registry",
607
+ tags=["solidworks-api", "vba", "typelib"],
608
+ )
609
+
610
+ sw_version = raw.get("solidworks_version") or "unknown"
611
+ logger.info(
612
+ "Built '%s' FAISS namespace: %d chunks (SW version: %s)",
613
+ namespace,
614
+ idx.chunk_count,
615
+ sw_version,
616
+ )
617
+ return idx
618
+
619
+
620
+ def query_solidworks_api_docs(
621
+ query: str,
622
+ *,
623
+ namespace: str = SW_API_DOCS_NAMESPACE,
624
+ top_k: int = 5,
625
+ rag_dir: Path | None = None,
626
+ score_threshold: float = 0.20,
627
+ ) -> str:
628
+ """Semantic search over the SolidWorks COM/VBA API surface.
629
+
630
+ Returns a formatted markdown context string ready to inject into an LLM system prompt,
631
+ or an empty string if no relevant results are found.
632
+
633
+ Parameters ---------- query: Natural-language question or task description. top_k:
634
+ Maximum number of chunks to return. rag_dir: Override for the FAISS storage directory.
635
+ score_threshold: Minimum cosine-similarity score (0–1) to include a chunk.
636
+
637
+ Args:
638
+ query (str): Query text used for the operation.
639
+ top_k (int): Maximum number of matches to return. Defaults to 5.
640
+ rag_dir (Path | None): Directory where RAG assets are stored. Defaults to None.
641
+ score_threshold (float): The score threshold value. Defaults to 0.20.
642
+
643
+ Returns:
644
+ str: The resulting text value.
645
+ """
646
+ try:
647
+ idx = VectorRAGIndex.load(namespace=namespace, rag_dir=rag_dir)
648
+ if idx.chunk_count == 0:
649
+ return _AwaitableQueryResult("", [])
650
+ hits = idx.query(query, top_k=top_k)
651
+ hits = [h for h in hits if h.get("score", 0) >= score_threshold]
652
+ if not hits:
653
+ return _AwaitableQueryResult("", [])
654
+ lines = ["## SolidWorks API Reference (from local COM/VBA index)"]
655
+ for i, hit in enumerate(hits, 1):
656
+ source = hit.get("source", "unknown")
657
+ text = hit["text"].strip()
658
+ lines.append(f"\n### [{i}] {source}")
659
+ lines.append(text)
660
+ return _AwaitableQueryResult("\n".join(lines), hits)
661
+ except ImportError:
662
+ return _AwaitableQueryResult("", []) # FAISS not installed
663
+ except Exception as exc:
664
+ logger.debug("query_solidworks_api_docs failed: %s", exc)
665
+ return _AwaitableQueryResult("", [])
@@ -0,0 +1,5 @@
1
+ """Caching utilities for SolidWorks MCP runtime services."""
2
+
3
+ from .response_cache import CachePolicy, ResponseCache
4
+
5
+ __all__ = ["CachePolicy", "ResponseCache"]