vector-inspector 0.2.6__py3-none-any.whl → 0.3.1__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.
- vector_inspector/config/__init__.py +4 -0
- vector_inspector/config/known_embedding_models.json +432 -0
- vector_inspector/core/cache_manager.py +159 -0
- vector_inspector/core/connection_manager.py +277 -0
- vector_inspector/core/connections/__init__.py +2 -1
- vector_inspector/core/connections/base_connection.py +42 -1
- vector_inspector/core/connections/chroma_connection.py +137 -16
- vector_inspector/core/connections/pinecone_connection.py +768 -0
- vector_inspector/core/connections/qdrant_connection.py +62 -8
- vector_inspector/core/embedding_providers/__init__.py +14 -0
- vector_inspector/core/embedding_providers/base_provider.py +128 -0
- vector_inspector/core/embedding_providers/clip_provider.py +260 -0
- vector_inspector/core/embedding_providers/provider_factory.py +176 -0
- vector_inspector/core/embedding_providers/sentence_transformer_provider.py +203 -0
- vector_inspector/core/embedding_utils.py +167 -0
- vector_inspector/core/model_registry.py +205 -0
- vector_inspector/services/backup_restore_service.py +19 -29
- vector_inspector/services/credential_service.py +130 -0
- vector_inspector/services/filter_service.py +1 -1
- vector_inspector/services/profile_service.py +409 -0
- vector_inspector/services/settings_service.py +136 -1
- vector_inspector/ui/components/connection_manager_panel.py +327 -0
- vector_inspector/ui/components/profile_manager_panel.py +565 -0
- vector_inspector/ui/dialogs/__init__.py +6 -0
- vector_inspector/ui/dialogs/cross_db_migration.py +383 -0
- vector_inspector/ui/dialogs/embedding_config_dialog.py +315 -0
- vector_inspector/ui/dialogs/provider_type_dialog.py +189 -0
- vector_inspector/ui/main_window.py +456 -190
- vector_inspector/ui/views/connection_view.py +55 -10
- vector_inspector/ui/views/info_panel.py +272 -55
- vector_inspector/ui/views/metadata_view.py +71 -3
- vector_inspector/ui/views/search_view.py +44 -4
- vector_inspector/ui/views/visualization_view.py +19 -5
- {vector_inspector-0.2.6.dist-info → vector_inspector-0.3.1.dist-info}/METADATA +3 -1
- vector_inspector-0.3.1.dist-info/RECORD +55 -0
- vector_inspector-0.2.6.dist-info/RECORD +0 -35
- {vector_inspector-0.2.6.dist-info → vector_inspector-0.3.1.dist-info}/WHEEL +0 -0
- {vector_inspector-0.2.6.dist-info → vector_inspector-0.3.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""Cross-database operations for migrating data between vector databases."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, List, Dict, Any
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import tempfile
|
|
6
|
+
from PySide6.QtWidgets import (
|
|
7
|
+
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
|
|
8
|
+
QPushButton, QProgressBar, QTextEdit, QGroupBox, QFormLayout,
|
|
9
|
+
QSpinBox, QCheckBox, QMessageBox
|
|
10
|
+
)
|
|
11
|
+
from PySide6.QtCore import QThread, Signal
|
|
12
|
+
|
|
13
|
+
from vector_inspector.core.connection_manager import ConnectionManager, ConnectionInstance
|
|
14
|
+
from vector_inspector.services.backup_restore_service import BackupRestoreService
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MigrationThread(QThread):
|
|
18
|
+
"""Background thread for migrating data between databases using backup/restore."""
|
|
19
|
+
|
|
20
|
+
progress = Signal(int, str) # progress percentage, status message
|
|
21
|
+
finished = Signal(bool, str) # success, message
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
source_conn: ConnectionInstance,
|
|
26
|
+
target_conn: ConnectionInstance,
|
|
27
|
+
source_collection: str,
|
|
28
|
+
target_collection: str,
|
|
29
|
+
include_embeddings: bool
|
|
30
|
+
):
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.source_conn = source_conn
|
|
33
|
+
self.target_conn = target_conn
|
|
34
|
+
self.source_collection = source_collection
|
|
35
|
+
self.target_collection = target_collection
|
|
36
|
+
self.include_embeddings = include_embeddings
|
|
37
|
+
self._cancelled = False
|
|
38
|
+
self.backup_service = BackupRestoreService()
|
|
39
|
+
|
|
40
|
+
def cancel(self):
|
|
41
|
+
"""Cancel the migration."""
|
|
42
|
+
self._cancelled = True
|
|
43
|
+
|
|
44
|
+
def run(self):
|
|
45
|
+
"""Run the migration using backup and restore."""
|
|
46
|
+
temp_backup_path = None
|
|
47
|
+
try:
|
|
48
|
+
if self._cancelled:
|
|
49
|
+
self.finished.emit(False, "Migration cancelled by user.")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
# Ensure connections are active
|
|
53
|
+
if not self.source_conn.connection.is_connected:
|
|
54
|
+
self.finished.emit(False, "Source connection is not active.")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
if not self.target_conn.connection.is_connected:
|
|
58
|
+
self.finished.emit(False, "Target connection is not active.")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
# Create temporary directory for backup
|
|
62
|
+
temp_dir = tempfile.mkdtemp(prefix="vector_migration_")
|
|
63
|
+
|
|
64
|
+
# Step 1: Create backup of source collection
|
|
65
|
+
self.progress.emit(10, f"Creating backup of {self.source_collection}...")
|
|
66
|
+
|
|
67
|
+
temp_backup_path = self.backup_service.backup_collection(
|
|
68
|
+
self.source_conn.connection,
|
|
69
|
+
self.source_collection,
|
|
70
|
+
temp_dir,
|
|
71
|
+
include_embeddings=self.include_embeddings
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if not temp_backup_path:
|
|
75
|
+
self.finished.emit(False, "Failed to create backup.")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
if self._cancelled:
|
|
79
|
+
self.finished.emit(False, "Migration cancelled by user.")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
# Step 2: Restore to target collection
|
|
83
|
+
self.progress.emit(50, f"Restoring to {self.target_collection}...")
|
|
84
|
+
|
|
85
|
+
# Verify target connection before restore
|
|
86
|
+
if not self.target_conn.connection.is_connected:
|
|
87
|
+
# Try to reconnect
|
|
88
|
+
if not self.target_conn.connection.connect():
|
|
89
|
+
self.finished.emit(False, "Target connection lost. Please try again.")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Check if target collection exists
|
|
93
|
+
target_exists = self.target_collection in self.target_conn.collections
|
|
94
|
+
|
|
95
|
+
success = self.backup_service.restore_collection(
|
|
96
|
+
self.target_conn.connection,
|
|
97
|
+
temp_backup_path,
|
|
98
|
+
collection_name=self.target_collection,
|
|
99
|
+
overwrite=target_exists
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if self._cancelled:
|
|
103
|
+
self.finished.emit(False, "Migration cancelled by user.")
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
if success:
|
|
107
|
+
self.progress.emit(100, f"Migration complete!")
|
|
108
|
+
self.finished.emit(True, f"Successfully migrated {self.source_collection} to {self.target_collection}")
|
|
109
|
+
else:
|
|
110
|
+
# Clean up target collection on failure
|
|
111
|
+
try:
|
|
112
|
+
if self.target_collection in self.target_conn.connection.list_collections():
|
|
113
|
+
self.progress.emit(90, "Cleaning up failed migration...")
|
|
114
|
+
print(f"Cleaning up failed migration: deleting target collection '{self.target_collection}'")
|
|
115
|
+
self.target_conn.connection.delete_collection(self.target_collection)
|
|
116
|
+
except Exception as cleanup_error:
|
|
117
|
+
print(f"Warning: Failed to clean up target collection: {cleanup_error}")
|
|
118
|
+
|
|
119
|
+
self.finished.emit(False, "Failed to restore to target collection. Target collection cleaned up.")
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
import traceback
|
|
123
|
+
error_details = traceback.format_exc()
|
|
124
|
+
print(f"Migration error details:\n{error_details}")
|
|
125
|
+
|
|
126
|
+
# Clean up target collection on exception
|
|
127
|
+
try:
|
|
128
|
+
if self.target_conn and self.target_conn.connection.is_connected:
|
|
129
|
+
if self.target_collection in self.target_conn.connection.list_collections():
|
|
130
|
+
print(f"Cleaning up failed migration: deleting target collection '{self.target_collection}'")
|
|
131
|
+
self.target_conn.connection.delete_collection(self.target_collection)
|
|
132
|
+
except Exception as cleanup_error:
|
|
133
|
+
print(f"Warning: Failed to clean up target collection: {cleanup_error}")
|
|
134
|
+
|
|
135
|
+
self.finished.emit(False, f"Migration error: {str(e)}")
|
|
136
|
+
|
|
137
|
+
finally:
|
|
138
|
+
# Clean up temporary backup file
|
|
139
|
+
if temp_backup_path:
|
|
140
|
+
try:
|
|
141
|
+
Path(temp_backup_path).unlink()
|
|
142
|
+
# Also remove temp directory if empty
|
|
143
|
+
temp_dir = Path(temp_backup_path).parent
|
|
144
|
+
if temp_dir.exists() and not list(temp_dir.iterdir()):
|
|
145
|
+
temp_dir.rmdir()
|
|
146
|
+
except Exception:
|
|
147
|
+
pass # Ignore cleanup errors
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class CrossDatabaseMigrationDialog(QDialog):
|
|
151
|
+
"""Dialog for migrating data between vector databases."""
|
|
152
|
+
|
|
153
|
+
def __init__(self, connection_manager: ConnectionManager, parent=None):
|
|
154
|
+
super().__init__(parent)
|
|
155
|
+
self.connection_manager = connection_manager
|
|
156
|
+
self.migration_thread: Optional[MigrationThread] = None
|
|
157
|
+
|
|
158
|
+
self.setWindowTitle("Cross-Database Migration")
|
|
159
|
+
self.setMinimumWidth(600)
|
|
160
|
+
self.setMinimumHeight(400)
|
|
161
|
+
|
|
162
|
+
self._setup_ui()
|
|
163
|
+
self._populate_connections()
|
|
164
|
+
|
|
165
|
+
def _setup_ui(self):
|
|
166
|
+
"""Setup the UI."""
|
|
167
|
+
layout = QVBoxLayout(self)
|
|
168
|
+
|
|
169
|
+
# Source section
|
|
170
|
+
source_group = QGroupBox("Source")
|
|
171
|
+
source_layout = QFormLayout()
|
|
172
|
+
|
|
173
|
+
self.source_connection_combo = QComboBox()
|
|
174
|
+
self.source_connection_combo.currentIndexChanged.connect(self._on_source_connection_changed)
|
|
175
|
+
source_layout.addRow("Connection:", self.source_connection_combo)
|
|
176
|
+
|
|
177
|
+
self.source_collection_combo = QComboBox()
|
|
178
|
+
source_layout.addRow("Collection:", self.source_collection_combo)
|
|
179
|
+
|
|
180
|
+
source_group.setLayout(source_layout)
|
|
181
|
+
layout.addWidget(source_group)
|
|
182
|
+
|
|
183
|
+
# Target section
|
|
184
|
+
target_group = QGroupBox("Target")
|
|
185
|
+
target_layout = QFormLayout()
|
|
186
|
+
|
|
187
|
+
self.target_connection_combo = QComboBox()
|
|
188
|
+
self.target_connection_combo.currentIndexChanged.connect(self._on_target_connection_changed)
|
|
189
|
+
target_layout.addRow("Connection:", self.target_connection_combo)
|
|
190
|
+
|
|
191
|
+
self.target_collection_combo = QComboBox()
|
|
192
|
+
self.target_collection_combo.setEditable(True)
|
|
193
|
+
target_layout.addRow("Collection:", self.target_collection_combo)
|
|
194
|
+
|
|
195
|
+
self.create_new_check = QCheckBox("Create new collection if it doesn't exist")
|
|
196
|
+
self.create_new_check.setChecked(True)
|
|
197
|
+
target_layout.addRow("", self.create_new_check)
|
|
198
|
+
|
|
199
|
+
target_group.setLayout(target_layout)
|
|
200
|
+
layout.addWidget(target_group)
|
|
201
|
+
|
|
202
|
+
# Options
|
|
203
|
+
options_group = QGroupBox("Options")
|
|
204
|
+
options_layout = QFormLayout()
|
|
205
|
+
|
|
206
|
+
self.include_embeddings_check = QCheckBox("Include Embeddings")
|
|
207
|
+
self.include_embeddings_check.setChecked(True)
|
|
208
|
+
options_layout.addRow("", self.include_embeddings_check)
|
|
209
|
+
|
|
210
|
+
options_group.setLayout(options_layout)
|
|
211
|
+
layout.addWidget(options_group)
|
|
212
|
+
|
|
213
|
+
# Progress section
|
|
214
|
+
self.progress_bar = QProgressBar()
|
|
215
|
+
self.progress_bar.setRange(0, 100)
|
|
216
|
+
self.progress_bar.setValue(0)
|
|
217
|
+
layout.addWidget(self.progress_bar)
|
|
218
|
+
|
|
219
|
+
self.status_text = QTextEdit()
|
|
220
|
+
self.status_text.setReadOnly(True)
|
|
221
|
+
self.status_text.setMaximumHeight(100)
|
|
222
|
+
layout.addWidget(self.status_text)
|
|
223
|
+
|
|
224
|
+
# Buttons
|
|
225
|
+
button_layout = QHBoxLayout()
|
|
226
|
+
|
|
227
|
+
self.start_button = QPushButton("Start Migration")
|
|
228
|
+
self.start_button.clicked.connect(self._start_migration)
|
|
229
|
+
button_layout.addWidget(self.start_button)
|
|
230
|
+
|
|
231
|
+
self.cancel_button = QPushButton("Cancel")
|
|
232
|
+
self.cancel_button.clicked.connect(self._cancel_migration)
|
|
233
|
+
self.cancel_button.setEnabled(False)
|
|
234
|
+
button_layout.addWidget(self.cancel_button)
|
|
235
|
+
|
|
236
|
+
self.close_button = QPushButton("Close")
|
|
237
|
+
self.close_button.clicked.connect(self.close)
|
|
238
|
+
button_layout.addWidget(self.close_button)
|
|
239
|
+
|
|
240
|
+
layout.addLayout(button_layout)
|
|
241
|
+
|
|
242
|
+
def _populate_connections(self):
|
|
243
|
+
"""Populate connection dropdowns."""
|
|
244
|
+
connections = self.connection_manager.get_all_connections()
|
|
245
|
+
|
|
246
|
+
self.source_connection_combo.clear()
|
|
247
|
+
self.target_connection_combo.clear()
|
|
248
|
+
|
|
249
|
+
for conn in connections:
|
|
250
|
+
self.source_connection_combo.addItem(conn.get_display_name(), conn.id)
|
|
251
|
+
self.target_connection_combo.addItem(conn.get_display_name(), conn.id)
|
|
252
|
+
|
|
253
|
+
# Populate collections for first connection
|
|
254
|
+
if connections:
|
|
255
|
+
self._on_source_connection_changed(0)
|
|
256
|
+
self._on_target_connection_changed(0)
|
|
257
|
+
|
|
258
|
+
def _on_source_connection_changed(self, index: int):
|
|
259
|
+
"""Handle source connection change."""
|
|
260
|
+
connection_id = self.source_connection_combo.currentData()
|
|
261
|
+
if connection_id:
|
|
262
|
+
instance = self.connection_manager.get_connection(connection_id)
|
|
263
|
+
if instance:
|
|
264
|
+
self.source_collection_combo.clear()
|
|
265
|
+
self.source_collection_combo.addItems(instance.collections)
|
|
266
|
+
|
|
267
|
+
def _on_target_connection_changed(self, index: int):
|
|
268
|
+
"""Handle target connection change."""
|
|
269
|
+
connection_id = self.target_connection_combo.currentData()
|
|
270
|
+
if connection_id:
|
|
271
|
+
instance = self.connection_manager.get_connection(connection_id)
|
|
272
|
+
if instance:
|
|
273
|
+
self.target_collection_combo.clear()
|
|
274
|
+
self.target_collection_combo.addItems(instance.collections)
|
|
275
|
+
|
|
276
|
+
def _start_migration(self):
|
|
277
|
+
"""Start the migration."""
|
|
278
|
+
# Validate selection
|
|
279
|
+
source_conn_id = self.source_connection_combo.currentData()
|
|
280
|
+
target_conn_id = self.target_connection_combo.currentData()
|
|
281
|
+
|
|
282
|
+
if not source_conn_id or not target_conn_id:
|
|
283
|
+
QMessageBox.warning(self, "Invalid Selection", "Please select both source and target connections.")
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
if source_conn_id == target_conn_id:
|
|
287
|
+
source_coll = self.source_collection_combo.currentText()
|
|
288
|
+
target_coll = self.target_collection_combo.currentText()
|
|
289
|
+
if source_coll == target_coll:
|
|
290
|
+
QMessageBox.warning(
|
|
291
|
+
self,
|
|
292
|
+
"Invalid Selection",
|
|
293
|
+
"Source and target cannot be the same collection in the same connection."
|
|
294
|
+
)
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
source_conn = self.connection_manager.get_connection(source_conn_id)
|
|
298
|
+
target_conn = self.connection_manager.get_connection(target_conn_id)
|
|
299
|
+
|
|
300
|
+
if not source_conn or not target_conn:
|
|
301
|
+
QMessageBox.warning(self, "Error", "Failed to get connection instances.")
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
source_collection = self.source_collection_combo.currentText()
|
|
305
|
+
target_collection = self.target_collection_combo.currentText().strip()
|
|
306
|
+
|
|
307
|
+
if not source_collection or not target_collection:
|
|
308
|
+
QMessageBox.warning(self, "Invalid Selection", "Please select both source and target collections.")
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
# Check if target collection exists
|
|
312
|
+
target_exists = target_collection in target_conn.collections
|
|
313
|
+
|
|
314
|
+
# If target doesn't exist and we're not set to create, warn user
|
|
315
|
+
if not target_exists and not self.create_new_check.isChecked():
|
|
316
|
+
QMessageBox.warning(
|
|
317
|
+
self,
|
|
318
|
+
"Collection Does Not Exist",
|
|
319
|
+
f"Target collection '{target_collection}' does not exist.\n"
|
|
320
|
+
"Please check 'Create new collection' to allow automatic creation during migration."
|
|
321
|
+
)
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
# Confirm
|
|
325
|
+
action = "create and migrate" if not target_exists else "migrate"
|
|
326
|
+
reply = QMessageBox.question(
|
|
327
|
+
self,
|
|
328
|
+
"Confirm Migration",
|
|
329
|
+
f"Migrate data from:\n {source_conn.name}/{source_collection}\n"
|
|
330
|
+
f"to:\n {target_conn.name}/{target_collection}\n\n"
|
|
331
|
+
f"This will {action} all data. Continue?",
|
|
332
|
+
QMessageBox.Yes | QMessageBox.No
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if reply != QMessageBox.Yes:
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
# Start migration thread
|
|
339
|
+
self.migration_thread = MigrationThread(
|
|
340
|
+
source_conn=source_conn,
|
|
341
|
+
target_conn=target_conn,
|
|
342
|
+
source_collection=source_collection,
|
|
343
|
+
target_collection=target_collection,
|
|
344
|
+
include_embeddings=self.include_embeddings_check.isChecked()
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
self.migration_thread.progress.connect(self._on_migration_progress)
|
|
348
|
+
self.migration_thread.finished.connect(self._on_migration_finished)
|
|
349
|
+
|
|
350
|
+
self.start_button.setEnabled(False)
|
|
351
|
+
self.cancel_button.setEnabled(True)
|
|
352
|
+
self.close_button.setEnabled(False)
|
|
353
|
+
|
|
354
|
+
self.status_text.clear()
|
|
355
|
+
self.progress_bar.setValue(0)
|
|
356
|
+
|
|
357
|
+
self.migration_thread.start()
|
|
358
|
+
|
|
359
|
+
def _cancel_migration(self):
|
|
360
|
+
"""Cancel the migration."""
|
|
361
|
+
if self.migration_thread:
|
|
362
|
+
self.migration_thread.cancel()
|
|
363
|
+
self.status_text.append("Cancelling migration...")
|
|
364
|
+
|
|
365
|
+
def _on_migration_progress(self, progress: int, message: str):
|
|
366
|
+
"""Handle migration progress update."""
|
|
367
|
+
self.progress_bar.setValue(progress)
|
|
368
|
+
self.status_text.append(message)
|
|
369
|
+
|
|
370
|
+
def _on_migration_finished(self, success: bool, message: str):
|
|
371
|
+
"""Handle migration completion."""
|
|
372
|
+
self.status_text.append(message)
|
|
373
|
+
|
|
374
|
+
if success:
|
|
375
|
+
QMessageBox.information(self, "Success", message)
|
|
376
|
+
else:
|
|
377
|
+
QMessageBox.warning(self, "Failed", message)
|
|
378
|
+
|
|
379
|
+
self.start_button.setEnabled(True)
|
|
380
|
+
self.cancel_button.setEnabled(False)
|
|
381
|
+
self.close_button.setEnabled(True)
|
|
382
|
+
self.migration_thread = None
|
|
383
|
+
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Dialog for configuring embedding models for collections (Step 2: Model Selection)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Tuple
|
|
4
|
+
from PySide6.QtWidgets import (
|
|
5
|
+
QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
|
6
|
+
QComboBox, QPushButton, QGroupBox, QTextEdit,
|
|
7
|
+
QMessageBox, QLineEdit, QFormLayout
|
|
8
|
+
)
|
|
9
|
+
from PySide6.QtCore import Qt
|
|
10
|
+
|
|
11
|
+
from vector_inspector.core.embedding_utils import get_available_models_for_dimension
|
|
12
|
+
from vector_inspector.core.model_registry import get_model_registry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EmbeddingConfigDialog(QDialog):
|
|
16
|
+
"""Dialog for selecting embedding model for a collection."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, collection_name: str, vector_dimension: int,
|
|
19
|
+
provider_type: Optional[str] = None,
|
|
20
|
+
current_model: Optional[str] = None,
|
|
21
|
+
current_type: Optional[str] = None,
|
|
22
|
+
parent=None):
|
|
23
|
+
super().__init__(parent)
|
|
24
|
+
self.collection_name = collection_name
|
|
25
|
+
self.vector_dimension = vector_dimension
|
|
26
|
+
self.provider_type = provider_type # Filter by this type
|
|
27
|
+
self.current_model = current_model
|
|
28
|
+
self.current_type = current_type
|
|
29
|
+
self.selected_model = None
|
|
30
|
+
self.selected_type = None
|
|
31
|
+
|
|
32
|
+
# Determine title based on provider type
|
|
33
|
+
if provider_type == "custom":
|
|
34
|
+
title = "Enter Custom Model"
|
|
35
|
+
elif provider_type:
|
|
36
|
+
type_names = {
|
|
37
|
+
"sentence-transformer": "Sentence Transformers",
|
|
38
|
+
"clip": "CLIP Models",
|
|
39
|
+
"openai": "OpenAI API",
|
|
40
|
+
"cohere": "Cohere API",
|
|
41
|
+
"vertex-ai": "Google Vertex AI",
|
|
42
|
+
"voyage": "Voyage AI"
|
|
43
|
+
}
|
|
44
|
+
type_name = type_names.get(provider_type, provider_type.title())
|
|
45
|
+
title = f"Select Model: {type_name}"
|
|
46
|
+
else:
|
|
47
|
+
title = f"Configure Embedding Model - {collection_name}"
|
|
48
|
+
|
|
49
|
+
self.setWindowTitle(title)
|
|
50
|
+
self.setMinimumWidth(500)
|
|
51
|
+
self._setup_ui()
|
|
52
|
+
|
|
53
|
+
def _setup_ui(self):
|
|
54
|
+
"""Setup dialog UI."""
|
|
55
|
+
layout = QVBoxLayout(self)
|
|
56
|
+
|
|
57
|
+
# Handle custom model entry case
|
|
58
|
+
if self.provider_type == "custom":
|
|
59
|
+
self._setup_custom_ui(layout)
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
# Info section
|
|
63
|
+
info_group = QGroupBox("Collection Information")
|
|
64
|
+
info_layout = QVBoxLayout()
|
|
65
|
+
|
|
66
|
+
info_layout.addWidget(QLabel(f"<b>Collection:</b> {self.collection_name}"))
|
|
67
|
+
info_layout.addWidget(QLabel(f"<b>Vector Dimension:</b> {self.vector_dimension}"))
|
|
68
|
+
|
|
69
|
+
if self.current_model:
|
|
70
|
+
info_layout.addWidget(QLabel(f"<b>Current Model:</b> {self.current_model} ({self.current_type})"))
|
|
71
|
+
else:
|
|
72
|
+
warning = QLabel("⚠️ No embedding model configured - using automatic detection")
|
|
73
|
+
warning.setStyleSheet("color: orange;")
|
|
74
|
+
info_layout.addWidget(warning)
|
|
75
|
+
|
|
76
|
+
info_group.setLayout(info_layout)
|
|
77
|
+
layout.addWidget(info_group)
|
|
78
|
+
|
|
79
|
+
# Model selection section
|
|
80
|
+
model_group = QGroupBox("Embedding Model Selection")
|
|
81
|
+
model_layout = QVBoxLayout()
|
|
82
|
+
|
|
83
|
+
# Get available models for this dimension, filtered by provider type
|
|
84
|
+
if self.provider_type:
|
|
85
|
+
registry = get_model_registry()
|
|
86
|
+
registry_models = registry.get_models_by_dimension(self.vector_dimension)
|
|
87
|
+
filtered_models = [m for m in registry_models if m.type == self.provider_type]
|
|
88
|
+
available_models = [(m.name, m.type, m.description) for m in filtered_models]
|
|
89
|
+
|
|
90
|
+
# Add custom models from settings
|
|
91
|
+
try:
|
|
92
|
+
from ...services.settings_service import SettingsService
|
|
93
|
+
settings = SettingsService()
|
|
94
|
+
custom_models = settings.get_custom_embedding_models(self.vector_dimension)
|
|
95
|
+
for model in custom_models:
|
|
96
|
+
if model["type"] == self.provider_type:
|
|
97
|
+
available_models.append((
|
|
98
|
+
model["name"],
|
|
99
|
+
model["type"],
|
|
100
|
+
f"{model['description']} (custom)"
|
|
101
|
+
))
|
|
102
|
+
except Exception:
|
|
103
|
+
pass
|
|
104
|
+
else:
|
|
105
|
+
available_models = get_available_models_for_dimension(self.vector_dimension)
|
|
106
|
+
|
|
107
|
+
if available_models:
|
|
108
|
+
model_layout.addWidget(QLabel(f"Available models for {self.vector_dimension}-dimensional vectors:"))
|
|
109
|
+
|
|
110
|
+
self.model_combo = QComboBox()
|
|
111
|
+
for model_name, model_type, description in available_models:
|
|
112
|
+
display_text = f"{model_name} ({model_type}) - {description}"
|
|
113
|
+
self.model_combo.addItem(display_text, (model_name, model_type))
|
|
114
|
+
|
|
115
|
+
# Set current selection if it matches
|
|
116
|
+
if self.current_model and self.current_type:
|
|
117
|
+
for i in range(self.model_combo.count()):
|
|
118
|
+
model_name, model_type = self.model_combo.itemData(i)
|
|
119
|
+
if model_name == self.current_model and model_type == self.current_type:
|
|
120
|
+
self.model_combo.setCurrentIndex(i)
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
model_layout.addWidget(self.model_combo)
|
|
124
|
+
|
|
125
|
+
# Description area
|
|
126
|
+
desc_label = QLabel("<b>About the selected model:</b>")
|
|
127
|
+
model_layout.addWidget(desc_label)
|
|
128
|
+
|
|
129
|
+
self.description_text = QTextEdit()
|
|
130
|
+
self.description_text.setReadOnly(True)
|
|
131
|
+
self.description_text.setMaximumHeight(100)
|
|
132
|
+
self.description_text.setStyleSheet("background-color: #f5f5f5; border: 1px solid #ccc; color: #000000;")
|
|
133
|
+
model_layout.addWidget(self.description_text)
|
|
134
|
+
|
|
135
|
+
# Update description when selection changes
|
|
136
|
+
self.model_combo.currentIndexChanged.connect(self._update_description)
|
|
137
|
+
self._update_description()
|
|
138
|
+
|
|
139
|
+
else:
|
|
140
|
+
# No models for this type + dimension
|
|
141
|
+
type_name = self.provider_type or "any type"
|
|
142
|
+
warning = QLabel(f"⚠️ No models of type '{type_name}' available for {self.vector_dimension} dimensions.")
|
|
143
|
+
warning.setWordWrap(True)
|
|
144
|
+
model_layout.addWidget(warning)
|
|
145
|
+
|
|
146
|
+
registry = get_model_registry()
|
|
147
|
+
all_dims = registry.get_all_dimensions()
|
|
148
|
+
dims_text = "Available dimensions: " + ", ".join(str(d) for d in sorted(all_dims))
|
|
149
|
+
model_layout.addWidget(QLabel(dims_text))
|
|
150
|
+
|
|
151
|
+
self.model_combo = None
|
|
152
|
+
|
|
153
|
+
model_group.setLayout(model_layout)
|
|
154
|
+
layout.addWidget(model_group)
|
|
155
|
+
|
|
156
|
+
# Buttons
|
|
157
|
+
button_layout = QHBoxLayout()
|
|
158
|
+
button_layout.addStretch()
|
|
159
|
+
|
|
160
|
+
self.save_btn = QPushButton("Save Configuration")
|
|
161
|
+
self.save_btn.clicked.connect(self._on_save)
|
|
162
|
+
# Always enabled - user can choose from combo OR enter custom
|
|
163
|
+
self.save_btn.setEnabled(True)
|
|
164
|
+
|
|
165
|
+
self.clear_btn = QPushButton("Clear Configuration")
|
|
166
|
+
self.clear_btn.clicked.connect(self._clear_config)
|
|
167
|
+
self.clear_btn.setEnabled(self.current_model is not None)
|
|
168
|
+
|
|
169
|
+
cancel_btn = QPushButton("Cancel")
|
|
170
|
+
cancel_btn.clicked.connect(self.reject)
|
|
171
|
+
|
|
172
|
+
button_layout.addWidget(self.save_btn)
|
|
173
|
+
button_layout.addWidget(self.clear_btn)
|
|
174
|
+
button_layout.addWidget(cancel_btn)
|
|
175
|
+
|
|
176
|
+
layout.addLayout(button_layout)
|
|
177
|
+
|
|
178
|
+
def _setup_custom_ui(self, layout):
|
|
179
|
+
"""Setup UI for custom model entry."""
|
|
180
|
+
# Info section
|
|
181
|
+
info_group = QGroupBox("Collection Information")
|
|
182
|
+
info_layout = QVBoxLayout()
|
|
183
|
+
info_layout.addWidget(QLabel(f"<b>Collection:</b> {self.collection_name}"))
|
|
184
|
+
info_layout.addWidget(QLabel(f"<b>Vector Dimension:</b> {self.vector_dimension}"))
|
|
185
|
+
info_group.setLayout(info_layout)
|
|
186
|
+
layout.addWidget(info_group)
|
|
187
|
+
|
|
188
|
+
# Custom model entry section
|
|
189
|
+
custom_group = QGroupBox("Enter Custom Model Details")
|
|
190
|
+
custom_layout = QFormLayout()
|
|
191
|
+
|
|
192
|
+
self.custom_name_input = QLineEdit()
|
|
193
|
+
self.custom_name_input.setPlaceholderText("e.g., sentence-transformers/all-mpnet-base-v2")
|
|
194
|
+
custom_layout.addRow("Model Name:", self.custom_name_input)
|
|
195
|
+
|
|
196
|
+
self.custom_type_combo = QComboBox()
|
|
197
|
+
self.custom_type_combo.addItems(["sentence-transformer", "clip", "openai", "cohere", "vertex-ai", "voyage", "custom"])
|
|
198
|
+
custom_layout.addRow("Model Type:", self.custom_type_combo)
|
|
199
|
+
|
|
200
|
+
self.custom_desc_input = QLineEdit()
|
|
201
|
+
self.custom_desc_input.setPlaceholderText("Brief description (optional)")
|
|
202
|
+
custom_layout.addRow("Description:", self.custom_desc_input)
|
|
203
|
+
|
|
204
|
+
custom_note = QLabel("💡 Custom models will be saved and available for future use with this dimension.")
|
|
205
|
+
custom_note.setWordWrap(True)
|
|
206
|
+
custom_note.setStyleSheet("color: #666; font-size: 10px; padding: 4px;")
|
|
207
|
+
custom_layout.addRow(custom_note)
|
|
208
|
+
|
|
209
|
+
custom_group.setLayout(custom_layout)
|
|
210
|
+
layout.addWidget(custom_group)
|
|
211
|
+
|
|
212
|
+
# Buttons for custom entry
|
|
213
|
+
button_layout = QHBoxLayout()
|
|
214
|
+
button_layout.addStretch()
|
|
215
|
+
|
|
216
|
+
cancel_btn = QPushButton("Cancel")
|
|
217
|
+
cancel_btn.clicked.connect(self.reject)
|
|
218
|
+
|
|
219
|
+
save_btn = QPushButton("Save")
|
|
220
|
+
save_btn.clicked.connect(self._save_custom_model)
|
|
221
|
+
save_btn.setDefault(True)
|
|
222
|
+
|
|
223
|
+
button_layout.addWidget(cancel_btn)
|
|
224
|
+
button_layout.addWidget(save_btn)
|
|
225
|
+
|
|
226
|
+
layout.addLayout(button_layout)
|
|
227
|
+
|
|
228
|
+
# No combo or description for custom mode
|
|
229
|
+
self.model_combo = None
|
|
230
|
+
|
|
231
|
+
def _save_custom_model(self):
|
|
232
|
+
"""Save custom model entry."""
|
|
233
|
+
custom_name = self.custom_name_input.text().strip()
|
|
234
|
+
custom_desc = self.custom_desc_input.text().strip()
|
|
235
|
+
custom_type = self.custom_type_combo.currentText()
|
|
236
|
+
|
|
237
|
+
if not custom_name:
|
|
238
|
+
QMessageBox.warning(self, "Invalid Input", "Please enter a model name.")
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
# Save custom model to registry
|
|
242
|
+
from vector_inspector.services.settings_service import SettingsService
|
|
243
|
+
settings = SettingsService()
|
|
244
|
+
|
|
245
|
+
settings.add_custom_embedding_model(
|
|
246
|
+
model_name=custom_name,
|
|
247
|
+
dimension=self.vector_dimension,
|
|
248
|
+
model_type=custom_type,
|
|
249
|
+
description=custom_desc if custom_desc else f"Custom {custom_type} model"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Set selection to custom model
|
|
253
|
+
self.selected_model = custom_name
|
|
254
|
+
self.selected_type = custom_type
|
|
255
|
+
self.accept()
|
|
256
|
+
|
|
257
|
+
def _update_description(self):
|
|
258
|
+
"""Update the description text based on selected model."""
|
|
259
|
+
if not self.model_combo:
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
model_name, model_type = self.model_combo.currentData()
|
|
263
|
+
|
|
264
|
+
descriptions = {
|
|
265
|
+
"sentence-transformer": (
|
|
266
|
+
"Sentence-Transformers are text-only embedding models optimized for semantic similarity. "
|
|
267
|
+
"They work well for text search, clustering, and classification tasks."
|
|
268
|
+
),
|
|
269
|
+
"clip": (
|
|
270
|
+
"CLIP (Contrastive Language-Image Pre-training) is a multi-modal model that can embed both "
|
|
271
|
+
"text and images into the same vector space. This allows text queries to find semantically "
|
|
272
|
+
"similar images, and vice versa. Perfect for image search with text descriptions."
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
desc = descriptions.get(model_type, "Embedding model for vector similarity search.")
|
|
277
|
+
self.description_text.setPlainText(
|
|
278
|
+
f"Model: {model_name}\n"
|
|
279
|
+
f"Type: {model_type}\n"
|
|
280
|
+
f"Dimension: {self.vector_dimension}\n\n"
|
|
281
|
+
f"{desc}"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def _on_save(self):
|
|
285
|
+
"""Handle save button click."""
|
|
286
|
+
if self.model_combo and self.model_combo.currentData():
|
|
287
|
+
# Use combo selection
|
|
288
|
+
model_name, model_type = self.model_combo.currentData()
|
|
289
|
+
self.selected_model = model_name
|
|
290
|
+
self.selected_type = model_type
|
|
291
|
+
else:
|
|
292
|
+
QMessageBox.warning(self, "No Selection", "Please select a model from the list.")
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
self.accept()
|
|
296
|
+
|
|
297
|
+
def _clear_config(self):
|
|
298
|
+
"""Clear the embedding model configuration."""
|
|
299
|
+
reply = QMessageBox.question(
|
|
300
|
+
self,
|
|
301
|
+
"Clear Configuration",
|
|
302
|
+
"This will remove the custom embedding model configuration and use automatic detection. Continue?",
|
|
303
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
307
|
+
self.selected_model = None
|
|
308
|
+
self.selected_type = None
|
|
309
|
+
self.done(2) # Custom code for "clear"
|
|
310
|
+
|
|
311
|
+
def get_selection(self) -> Optional[Tuple[str, str]]:
|
|
312
|
+
"""Get the selected model and type (from either combo or custom entry)."""
|
|
313
|
+
if self.selected_model and self.selected_type:
|
|
314
|
+
return (self.selected_model, self.selected_type)
|
|
315
|
+
return None
|