vector-inspector 0.2.7__py3-none-any.whl → 0.3.2__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/connections/__init__.py +2 -1
- vector_inspector/core/connections/base_connection.py +42 -1
- vector_inspector/core/connections/chroma_connection.py +47 -11
- vector_inspector/core/connections/pinecone_connection.py +768 -0
- 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 +69 -42
- vector_inspector/core/model_registry.py +205 -0
- vector_inspector/services/backup_restore_service.py +16 -0
- vector_inspector/services/settings_service.py +117 -1
- vector_inspector/ui/components/connection_manager_panel.py +7 -0
- vector_inspector/ui/components/profile_manager_panel.py +61 -14
- vector_inspector/ui/dialogs/__init__.py +2 -1
- vector_inspector/ui/dialogs/cross_db_migration.py +20 -1
- vector_inspector/ui/dialogs/embedding_config_dialog.py +166 -27
- vector_inspector/ui/dialogs/provider_type_dialog.py +189 -0
- vector_inspector/ui/main_window.py +33 -2
- vector_inspector/ui/views/connection_view.py +55 -10
- vector_inspector/ui/views/info_panel.py +83 -36
- vector_inspector/ui/views/search_view.py +1 -1
- vector_inspector/ui/views/visualization_view.py +20 -6
- {vector_inspector-0.2.7.dist-info → vector_inspector-0.3.2.dist-info}/METADATA +7 -2
- vector_inspector-0.3.2.dist-info/RECORD +55 -0
- vector_inspector-0.2.7.dist-info/RECORD +0 -45
- {vector_inspector-0.2.7.dist-info → vector_inspector-0.3.2.dist-info}/WHEEL +0 -0
- {vector_inspector-0.2.7.dist-info → vector_inspector-0.3.2.dist-info}/entry_points.txt +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Dict, Any, Optional
|
|
5
|
+
from typing import Dict, Any, Optional, List
|
|
6
6
|
from vector_inspector.core.cache_manager import invalidate_cache_on_settings_change
|
|
7
7
|
|
|
8
8
|
|
|
@@ -77,3 +77,119 @@ class SettingsService:
|
|
|
77
77
|
"""Clear all settings."""
|
|
78
78
|
self.settings = {}
|
|
79
79
|
self._save_settings()
|
|
80
|
+
|
|
81
|
+
def save_embedding_model(self, connection_id: str, collection_name: str, model_name: str, model_type: str = "user-configured"):
|
|
82
|
+
"""Save embedding model mapping for a collection.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
connection_id: Connection identifier
|
|
86
|
+
collection_name: Collection name
|
|
87
|
+
model_name: Embedding model name (e.g., 'sentence-transformers/all-MiniLM-L6-v2')
|
|
88
|
+
model_type: Type of configuration ('user-configured', 'auto-detected', 'stored')
|
|
89
|
+
"""
|
|
90
|
+
if "collection_embedding_models" not in self.settings:
|
|
91
|
+
self.settings["collection_embedding_models"] = {}
|
|
92
|
+
|
|
93
|
+
collection_key = f"{connection_id}:{collection_name}"
|
|
94
|
+
self.settings["collection_embedding_models"][collection_key] = {
|
|
95
|
+
"model": model_name,
|
|
96
|
+
"type": model_type,
|
|
97
|
+
"timestamp": self._get_timestamp()
|
|
98
|
+
}
|
|
99
|
+
self._save_settings()
|
|
100
|
+
|
|
101
|
+
def get_embedding_model(self, connection_id: str, collection_name: str) -> Optional[Dict[str, Any]]:
|
|
102
|
+
"""Get embedding model mapping for a collection.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
connection_id: Connection identifier
|
|
106
|
+
collection_name: Collection name
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Dictionary with 'model', 'type', and 'timestamp' or None
|
|
110
|
+
"""
|
|
111
|
+
collection_models = self.settings.get("collection_embedding_models", {})
|
|
112
|
+
collection_key = f"{connection_id}:{collection_name}"
|
|
113
|
+
return collection_models.get(collection_key)
|
|
114
|
+
|
|
115
|
+
def remove_embedding_model(self, connection_id: str, collection_name: str):
|
|
116
|
+
"""Remove embedding model mapping for a collection.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
connection_id: Connection identifier
|
|
120
|
+
collection_name: Collection name
|
|
121
|
+
"""
|
|
122
|
+
if "collection_embedding_models" not in self.settings:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
collection_key = f"{connection_id}:{collection_name}"
|
|
126
|
+
self.settings["collection_embedding_models"].pop(collection_key, None)
|
|
127
|
+
self._save_settings()
|
|
128
|
+
|
|
129
|
+
def _get_timestamp(self) -> str:
|
|
130
|
+
"""Get current timestamp as ISO string."""
|
|
131
|
+
from datetime import datetime
|
|
132
|
+
return datetime.now().isoformat()
|
|
133
|
+
|
|
134
|
+
def add_custom_embedding_model(self, model_name: str, dimension: int, model_type: str = "sentence-transformer", description: str = "Custom model"):
|
|
135
|
+
"""Add a custom embedding model to the known models list.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
model_name: Name of the embedding model
|
|
139
|
+
dimension: Vector dimension
|
|
140
|
+
model_type: Type of model (e.g., 'sentence-transformer', 'clip', 'openai')
|
|
141
|
+
description: Brief description of the model
|
|
142
|
+
"""
|
|
143
|
+
if "custom_embedding_models" not in self.settings:
|
|
144
|
+
self.settings["custom_embedding_models"] = []
|
|
145
|
+
|
|
146
|
+
# Check if already exists
|
|
147
|
+
for model in self.settings["custom_embedding_models"]:
|
|
148
|
+
if model["name"] == model_name and model["dimension"] == dimension:
|
|
149
|
+
# Update existing entry
|
|
150
|
+
model["type"] = model_type
|
|
151
|
+
model["description"] = description
|
|
152
|
+
model["last_used"] = self._get_timestamp()
|
|
153
|
+
self._save_settings()
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# Add new entry
|
|
157
|
+
self.settings["custom_embedding_models"].append({
|
|
158
|
+
"name": model_name,
|
|
159
|
+
"dimension": dimension,
|
|
160
|
+
"type": model_type,
|
|
161
|
+
"description": description,
|
|
162
|
+
"added": self._get_timestamp(),
|
|
163
|
+
"last_used": self._get_timestamp()
|
|
164
|
+
})
|
|
165
|
+
self._save_settings()
|
|
166
|
+
|
|
167
|
+
def get_custom_embedding_models(self, dimension: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
168
|
+
"""Get list of custom embedding models.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
dimension: Optional filter by dimension
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
List of custom model dictionaries
|
|
175
|
+
"""
|
|
176
|
+
models = self.settings.get("custom_embedding_models", [])
|
|
177
|
+
if dimension is not None:
|
|
178
|
+
return [m for m in models if m["dimension"] == dimension]
|
|
179
|
+
return models
|
|
180
|
+
|
|
181
|
+
def remove_custom_embedding_model(self, model_name: str, dimension: int):
|
|
182
|
+
"""Remove a custom embedding model.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
model_name: Name of the model to remove
|
|
186
|
+
dimension: Vector dimension
|
|
187
|
+
"""
|
|
188
|
+
if "custom_embedding_models" not in self.settings:
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
self.settings["custom_embedding_models"] = [
|
|
192
|
+
m for m in self.settings["custom_embedding_models"]
|
|
193
|
+
if not (m["name"] == model_name and m["dimension"] == dimension)
|
|
194
|
+
]
|
|
195
|
+
self._save_settings()
|
|
@@ -289,11 +289,18 @@ class ConnectionManagerPanel(QWidget):
|
|
|
289
289
|
if not instance or not instance.connection.is_connected:
|
|
290
290
|
return
|
|
291
291
|
|
|
292
|
+
# Show loading while refreshing
|
|
293
|
+
from ..components.loading_dialog import LoadingDialog
|
|
294
|
+
loading = LoadingDialog("Refreshing collections...", self)
|
|
295
|
+
loading.show_loading("Refreshing collections...")
|
|
296
|
+
QApplication.processEvents()
|
|
292
297
|
try:
|
|
293
298
|
collections = instance.connection.list_collections()
|
|
294
299
|
self.connection_manager.update_collections(connection_id, collections)
|
|
295
300
|
except Exception as e:
|
|
296
301
|
QMessageBox.warning(self, "Error", f"Failed to refresh collections: {e}")
|
|
302
|
+
finally:
|
|
303
|
+
loading.hide_loading()
|
|
297
304
|
|
|
298
305
|
def _disconnect_connection(self, connection_id: str):
|
|
299
306
|
"""Disconnect a connection."""
|
|
@@ -266,6 +266,7 @@ class ProfileEditorDialog(QDialog):
|
|
|
266
266
|
self.provider_combo = QComboBox()
|
|
267
267
|
self.provider_combo.addItem("ChromaDB", "chromadb")
|
|
268
268
|
self.provider_combo.addItem("Qdrant", "qdrant")
|
|
269
|
+
self.provider_combo.addItem("Pinecone", "pinecone")
|
|
269
270
|
self.provider_combo.currentIndexChanged.connect(self._on_provider_changed)
|
|
270
271
|
form_layout.addRow("Provider:", self.provider_combo)
|
|
271
272
|
|
|
@@ -356,23 +357,48 @@ class ProfileEditorDialog(QDialog):
|
|
|
356
357
|
if self.port_input.text() == "6333":
|
|
357
358
|
self.port_input.setText("8000")
|
|
358
359
|
|
|
359
|
-
#
|
|
360
|
-
|
|
361
|
-
|
|
360
|
+
# For Pinecone, disable persistent/ephemeral modes and only show API key
|
|
361
|
+
if provider == "pinecone":
|
|
362
|
+
self.persistent_radio.setEnabled(False)
|
|
363
|
+
self.http_radio.setEnabled(True)
|
|
364
|
+
self.http_radio.setChecked(True)
|
|
365
|
+
self.ephemeral_radio.setEnabled(False)
|
|
366
|
+
self.path_input.setEnabled(False)
|
|
367
|
+
self.path_browse_btn.setEnabled(False)
|
|
368
|
+
self.host_input.setEnabled(False)
|
|
369
|
+
self.port_input.setEnabled(False)
|
|
370
|
+
self.api_key_input.setEnabled(True)
|
|
371
|
+
else:
|
|
372
|
+
self.persistent_radio.setEnabled(True)
|
|
373
|
+
self.http_radio.setEnabled(True)
|
|
374
|
+
self.ephemeral_radio.setEnabled(True)
|
|
375
|
+
# Show/hide API key field
|
|
376
|
+
is_http = self.http_radio.isChecked()
|
|
377
|
+
self.api_key_input.setEnabled(is_http and provider == "qdrant")
|
|
378
|
+
# Update other fields based on connection type
|
|
379
|
+
self._on_type_changed()
|
|
362
380
|
|
|
363
381
|
def _on_type_changed(self):
|
|
364
382
|
"""Handle connection type change."""
|
|
365
383
|
is_persistent = self.persistent_radio.isChecked()
|
|
366
384
|
is_http = self.http_radio.isChecked()
|
|
367
385
|
|
|
368
|
-
# Show/hide relevant fields
|
|
369
|
-
self.path_input.setEnabled(is_persistent)
|
|
370
|
-
self.path_browse_btn.setEnabled(is_persistent)
|
|
371
|
-
self.host_input.setEnabled(is_http)
|
|
372
|
-
self.port_input.setEnabled(is_http)
|
|
373
|
-
|
|
374
386
|
provider = self.provider_combo.currentData()
|
|
375
|
-
|
|
387
|
+
|
|
388
|
+
# Pinecone always uses API key, no path/host/port
|
|
389
|
+
if provider == "pinecone":
|
|
390
|
+
self.path_input.setEnabled(False)
|
|
391
|
+
self.path_browse_btn.setEnabled(False)
|
|
392
|
+
self.host_input.setEnabled(False)
|
|
393
|
+
self.port_input.setEnabled(False)
|
|
394
|
+
self.api_key_input.setEnabled(True)
|
|
395
|
+
else:
|
|
396
|
+
# Show/hide relevant fields
|
|
397
|
+
self.path_input.setEnabled(is_persistent)
|
|
398
|
+
self.path_browse_btn.setEnabled(is_persistent)
|
|
399
|
+
self.host_input.setEnabled(is_http)
|
|
400
|
+
self.port_input.setEnabled(is_http)
|
|
401
|
+
self.api_key_input.setEnabled(is_http and provider == "qdrant")
|
|
376
402
|
|
|
377
403
|
def _browse_for_path(self):
|
|
378
404
|
"""Browse for persistent storage path."""
|
|
@@ -405,7 +431,10 @@ class ProfileEditorDialog(QDialog):
|
|
|
405
431
|
conn_type = config.get("type", "persistent")
|
|
406
432
|
|
|
407
433
|
# Set connection type
|
|
408
|
-
if conn_type == "
|
|
434
|
+
if conn_type == "cloud":
|
|
435
|
+
# Pinecone cloud connection
|
|
436
|
+
self.http_radio.setChecked(True)
|
|
437
|
+
elif conn_type == "persistent":
|
|
409
438
|
self.persistent_radio.setChecked(True)
|
|
410
439
|
self.path_input.setText(config.get("path", ""))
|
|
411
440
|
elif conn_type == "http":
|
|
@@ -429,9 +458,16 @@ class ProfileEditorDialog(QDialog):
|
|
|
429
458
|
# Create connection
|
|
430
459
|
from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
|
|
431
460
|
from vector_inspector.core.connections.qdrant_connection import QdrantConnection
|
|
461
|
+
from vector_inspector.core.connections.pinecone_connection import PineconeConnection
|
|
432
462
|
|
|
433
463
|
try:
|
|
434
|
-
if provider == "
|
|
464
|
+
if provider == "pinecone":
|
|
465
|
+
api_key = self.api_key_input.text()
|
|
466
|
+
if not api_key:
|
|
467
|
+
QMessageBox.warning(self, "Missing API Key", "Pinecone requires an API key.")
|
|
468
|
+
return
|
|
469
|
+
conn = PineconeConnection(api_key=api_key)
|
|
470
|
+
elif provider == "chromadb":
|
|
435
471
|
conn = ChromaDBConnection(**self._get_connection_kwargs(config))
|
|
436
472
|
else:
|
|
437
473
|
conn = QdrantConnection(**self._get_connection_kwargs(config))
|
|
@@ -455,8 +491,12 @@ class ProfileEditorDialog(QDialog):
|
|
|
455
491
|
def _get_config(self) -> dict:
|
|
456
492
|
"""Get configuration from form."""
|
|
457
493
|
config = {}
|
|
494
|
+
provider = self.provider_combo.currentData()
|
|
458
495
|
|
|
459
|
-
|
|
496
|
+
# Pinecone uses cloud connection type
|
|
497
|
+
if provider == "pinecone":
|
|
498
|
+
config["type"] = "cloud"
|
|
499
|
+
elif self.persistent_radio.isChecked():
|
|
460
500
|
config["type"] = "persistent"
|
|
461
501
|
config["path"] = self.path_input.text()
|
|
462
502
|
elif self.http_radio.isChecked():
|
|
@@ -494,7 +534,14 @@ class ProfileEditorDialog(QDialog):
|
|
|
494
534
|
|
|
495
535
|
# Get credentials
|
|
496
536
|
credentials = {}
|
|
497
|
-
if
|
|
537
|
+
if provider == "pinecone":
|
|
538
|
+
# Pinecone always requires API key
|
|
539
|
+
if self.api_key_input.text():
|
|
540
|
+
credentials["api_key"] = self.api_key_input.text()
|
|
541
|
+
else:
|
|
542
|
+
QMessageBox.warning(self, "Missing API Key", "Pinecone requires an API key.")
|
|
543
|
+
return
|
|
544
|
+
elif self.api_key_input.text() and self.http_radio.isChecked():
|
|
498
545
|
credentials["api_key"] = self.api_key_input.text()
|
|
499
546
|
|
|
500
547
|
if self.is_edit_mode:
|
|
@@ -107,12 +107,31 @@ class MigrationThread(QThread):
|
|
|
107
107
|
self.progress.emit(100, f"Migration complete!")
|
|
108
108
|
self.finished.emit(True, f"Successfully migrated {self.source_collection} to {self.target_collection}")
|
|
109
109
|
else:
|
|
110
|
-
|
|
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.")
|
|
111
120
|
|
|
112
121
|
except Exception as e:
|
|
113
122
|
import traceback
|
|
114
123
|
error_details = traceback.format_exc()
|
|
115
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
|
+
|
|
116
135
|
self.finished.emit(False, f"Migration error: {str(e)}")
|
|
117
136
|
|
|
118
137
|
finally:
|
|
@@ -1,38 +1,63 @@
|
|
|
1
|
-
"""Dialog for configuring embedding models for collections."""
|
|
1
|
+
"""Dialog for configuring embedding models for collections (Step 2: Model Selection)."""
|
|
2
2
|
|
|
3
3
|
from typing import Optional, Tuple
|
|
4
4
|
from PySide6.QtWidgets import (
|
|
5
5
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
|
6
6
|
QComboBox, QPushButton, QGroupBox, QTextEdit,
|
|
7
|
-
QMessageBox
|
|
7
|
+
QMessageBox, QLineEdit, QFormLayout
|
|
8
8
|
)
|
|
9
9
|
from PySide6.QtCore import Qt
|
|
10
10
|
|
|
11
|
-
from vector_inspector.core.embedding_utils import get_available_models_for_dimension
|
|
11
|
+
from vector_inspector.core.embedding_utils import get_available_models_for_dimension
|
|
12
|
+
from vector_inspector.core.model_registry import get_model_registry
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class EmbeddingConfigDialog(QDialog):
|
|
15
16
|
"""Dialog for selecting embedding model for a collection."""
|
|
16
17
|
|
|
17
18
|
def __init__(self, collection_name: str, vector_dimension: int,
|
|
19
|
+
provider_type: Optional[str] = None,
|
|
18
20
|
current_model: Optional[str] = None,
|
|
19
21
|
current_type: Optional[str] = None,
|
|
20
22
|
parent=None):
|
|
21
23
|
super().__init__(parent)
|
|
22
24
|
self.collection_name = collection_name
|
|
23
25
|
self.vector_dimension = vector_dimension
|
|
26
|
+
self.provider_type = provider_type # Filter by this type
|
|
24
27
|
self.current_model = current_model
|
|
25
28
|
self.current_type = current_type
|
|
26
29
|
self.selected_model = None
|
|
27
30
|
self.selected_type = None
|
|
28
31
|
|
|
29
|
-
|
|
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)
|
|
30
50
|
self.setMinimumWidth(500)
|
|
31
51
|
self._setup_ui()
|
|
32
52
|
|
|
33
53
|
def _setup_ui(self):
|
|
34
54
|
"""Setup dialog UI."""
|
|
35
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
|
|
36
61
|
|
|
37
62
|
# Info section
|
|
38
63
|
info_group = QGroupBox("Collection Information")
|
|
@@ -55,17 +80,38 @@ class EmbeddingConfigDialog(QDialog):
|
|
|
55
80
|
model_group = QGroupBox("Embedding Model Selection")
|
|
56
81
|
model_layout = QVBoxLayout()
|
|
57
82
|
|
|
58
|
-
# Get available models for this dimension
|
|
59
|
-
|
|
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)
|
|
60
106
|
|
|
61
107
|
if available_models:
|
|
62
108
|
model_layout.addWidget(QLabel(f"Available models for {self.vector_dimension}-dimensional vectors:"))
|
|
63
|
-
|
|
109
|
+
|
|
64
110
|
self.model_combo = QComboBox()
|
|
65
111
|
for model_name, model_type, description in available_models:
|
|
66
112
|
display_text = f"{model_name} ({model_type}) - {description}"
|
|
67
113
|
self.model_combo.addItem(display_text, (model_name, model_type))
|
|
68
|
-
|
|
114
|
+
|
|
69
115
|
# Set current selection if it matches
|
|
70
116
|
if self.current_model and self.current_type:
|
|
71
117
|
for i in range(self.model_combo.count()):
|
|
@@ -73,33 +119,35 @@ class EmbeddingConfigDialog(QDialog):
|
|
|
73
119
|
if model_name == self.current_model and model_type == self.current_type:
|
|
74
120
|
self.model_combo.setCurrentIndex(i)
|
|
75
121
|
break
|
|
76
|
-
|
|
122
|
+
|
|
77
123
|
model_layout.addWidget(self.model_combo)
|
|
78
|
-
|
|
124
|
+
|
|
79
125
|
# Description area
|
|
80
126
|
desc_label = QLabel("<b>About the selected model:</b>")
|
|
81
127
|
model_layout.addWidget(desc_label)
|
|
82
|
-
|
|
128
|
+
|
|
83
129
|
self.description_text = QTextEdit()
|
|
84
130
|
self.description_text.setReadOnly(True)
|
|
85
131
|
self.description_text.setMaximumHeight(100)
|
|
86
132
|
self.description_text.setStyleSheet("background-color: #f5f5f5; border: 1px solid #ccc; color: #000000;")
|
|
87
133
|
model_layout.addWidget(self.description_text)
|
|
88
|
-
|
|
134
|
+
|
|
89
135
|
# Update description when selection changes
|
|
90
136
|
self.model_combo.currentIndexChanged.connect(self._update_description)
|
|
91
137
|
self._update_description()
|
|
92
|
-
|
|
138
|
+
|
|
93
139
|
else:
|
|
94
|
-
# No
|
|
95
|
-
|
|
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.")
|
|
96
143
|
warning.setWordWrap(True)
|
|
97
144
|
model_layout.addWidget(warning)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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))
|
|
101
149
|
model_layout.addWidget(QLabel(dims_text))
|
|
102
|
-
|
|
150
|
+
|
|
103
151
|
self.model_combo = None
|
|
104
152
|
|
|
105
153
|
model_group.setLayout(model_layout)
|
|
@@ -110,8 +158,9 @@ class EmbeddingConfigDialog(QDialog):
|
|
|
110
158
|
button_layout.addStretch()
|
|
111
159
|
|
|
112
160
|
self.save_btn = QPushButton("Save Configuration")
|
|
113
|
-
self.save_btn.clicked.connect(self.
|
|
114
|
-
|
|
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)
|
|
115
164
|
|
|
116
165
|
self.clear_btn = QPushButton("Clear Configuration")
|
|
117
166
|
self.clear_btn.clicked.connect(self._clear_config)
|
|
@@ -126,6 +175,85 @@ class EmbeddingConfigDialog(QDialog):
|
|
|
126
175
|
|
|
127
176
|
layout.addLayout(button_layout)
|
|
128
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
|
+
|
|
129
257
|
def _update_description(self):
|
|
130
258
|
"""Update the description text based on selected model."""
|
|
131
259
|
if not self.model_combo:
|
|
@@ -153,6 +281,19 @@ class EmbeddingConfigDialog(QDialog):
|
|
|
153
281
|
f"{desc}"
|
|
154
282
|
)
|
|
155
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
|
+
|
|
156
297
|
def _clear_config(self):
|
|
157
298
|
"""Clear the embedding model configuration."""
|
|
158
299
|
reply = QMessageBox.question(
|
|
@@ -168,9 +309,7 @@ class EmbeddingConfigDialog(QDialog):
|
|
|
168
309
|
self.done(2) # Custom code for "clear"
|
|
169
310
|
|
|
170
311
|
def get_selection(self) -> Optional[Tuple[str, str]]:
|
|
171
|
-
"""Get the selected model and type."""
|
|
172
|
-
if
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
model_name, model_type = self.model_combo.currentData()
|
|
176
|
-
return (model_name, model_type)
|
|
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
|