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,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("", [])
|