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
|
@@ -10,6 +10,7 @@ from PySide6.QtCore import Signal, QThread
|
|
|
10
10
|
from vector_inspector.core.connections.base_connection import VectorDBConnection
|
|
11
11
|
from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
|
|
12
12
|
from vector_inspector.core.connections.qdrant_connection import QdrantConnection
|
|
13
|
+
from vector_inspector.core.connections.pinecone_connection import PineconeConnection
|
|
13
14
|
from vector_inspector.ui.components.loading_dialog import LoadingDialog
|
|
14
15
|
from vector_inspector.services.settings_service import SettingsService
|
|
15
16
|
|
|
@@ -66,6 +67,7 @@ class ConnectionDialog(QDialog):
|
|
|
66
67
|
self.provider_combo = QComboBox()
|
|
67
68
|
self.provider_combo.addItem("ChromaDB", "chromadb")
|
|
68
69
|
self.provider_combo.addItem("Qdrant", "qdrant")
|
|
70
|
+
self.provider_combo.addItem("Pinecone", "pinecone")
|
|
69
71
|
self.provider_combo.currentIndexChanged.connect(self._on_provider_changed)
|
|
70
72
|
provider_layout.addWidget(self.provider_combo)
|
|
71
73
|
provider_group.setLayout(provider_layout)
|
|
@@ -179,19 +181,42 @@ class ConnectionDialog(QDialog):
|
|
|
179
181
|
if self.port_input.text() == "6333":
|
|
180
182
|
self.port_input.setText("8000")
|
|
181
183
|
|
|
182
|
-
#
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
# For Pinecone, hide persistent/HTTP options and only show API key
|
|
185
|
+
if self.provider == "pinecone":
|
|
186
|
+
self.persistent_radio.setEnabled(False)
|
|
187
|
+
self.http_radio.setEnabled(True)
|
|
188
|
+
self.http_radio.setChecked(True)
|
|
189
|
+
self.ephemeral_radio.setEnabled(False)
|
|
190
|
+
self.path_input.setEnabled(False)
|
|
191
|
+
self.host_input.setEnabled(False)
|
|
192
|
+
self.port_input.setEnabled(False)
|
|
193
|
+
self.api_key_input.setEnabled(True)
|
|
194
|
+
else:
|
|
195
|
+
self.persistent_radio.setEnabled(True)
|
|
196
|
+
self.http_radio.setEnabled(True)
|
|
197
|
+
self.ephemeral_radio.setEnabled(True)
|
|
198
|
+
# Show/hide API key field
|
|
199
|
+
is_http = self.http_radio.isChecked()
|
|
200
|
+
self.api_key_input.setEnabled(is_http and self.provider == "qdrant")
|
|
201
|
+
# Update path/host/port based on connection type
|
|
202
|
+
self._on_type_changed()
|
|
185
203
|
|
|
186
204
|
def _on_type_changed(self):
|
|
187
205
|
"""Handle connection type change."""
|
|
188
206
|
is_persistent = self.persistent_radio.isChecked()
|
|
189
207
|
is_http = self.http_radio.isChecked()
|
|
190
208
|
|
|
191
|
-
|
|
192
|
-
self.
|
|
193
|
-
|
|
194
|
-
|
|
209
|
+
# Pinecone always uses API key, no path/host/port
|
|
210
|
+
if self.provider == "pinecone":
|
|
211
|
+
self.path_input.setEnabled(False)
|
|
212
|
+
self.host_input.setEnabled(False)
|
|
213
|
+
self.port_input.setEnabled(False)
|
|
214
|
+
self.api_key_input.setEnabled(True)
|
|
215
|
+
else:
|
|
216
|
+
self.path_input.setEnabled(is_persistent)
|
|
217
|
+
self.host_input.setEnabled(is_http)
|
|
218
|
+
self.port_input.setEnabled(is_http)
|
|
219
|
+
self.api_key_input.setEnabled(is_http and self.provider == "qdrant")
|
|
195
220
|
|
|
196
221
|
self._update_absolute_preview()
|
|
197
222
|
|
|
@@ -202,7 +227,13 @@ class ConnectionDialog(QDialog):
|
|
|
202
227
|
|
|
203
228
|
config = {"provider": self.provider}
|
|
204
229
|
|
|
205
|
-
|
|
230
|
+
# Pinecone only needs API key
|
|
231
|
+
if self.provider == "pinecone":
|
|
232
|
+
config.update({
|
|
233
|
+
"type": "cloud",
|
|
234
|
+
"api_key": self.api_key_input.text()
|
|
235
|
+
})
|
|
236
|
+
elif self.persistent_radio.isChecked():
|
|
206
237
|
config.update({"type": "persistent", "path": self.path_input.text()})
|
|
207
238
|
elif self.http_radio.isChecked():
|
|
208
239
|
config.update({
|
|
@@ -286,7 +317,13 @@ class ConnectionDialog(QDialog):
|
|
|
286
317
|
|
|
287
318
|
# Set connection type
|
|
288
319
|
conn_type = last_config.get("type", "persistent")
|
|
289
|
-
if conn_type == "
|
|
320
|
+
if conn_type == "cloud":
|
|
321
|
+
# Pinecone cloud connection
|
|
322
|
+
self.http_radio.setChecked(True)
|
|
323
|
+
api_key = last_config.get("api_key")
|
|
324
|
+
if api_key:
|
|
325
|
+
self.api_key_input.setText(api_key)
|
|
326
|
+
elif conn_type == "persistent":
|
|
290
327
|
self.persistent_radio.setChecked(True)
|
|
291
328
|
path = last_config.get("path", "")
|
|
292
329
|
if path:
|
|
@@ -370,7 +407,15 @@ class ConnectionView(QWidget):
|
|
|
370
407
|
conn_type = config.get("type")
|
|
371
408
|
|
|
372
409
|
# Create appropriate connection instance based on provider
|
|
373
|
-
if provider == "
|
|
410
|
+
if provider == "pinecone":
|
|
411
|
+
api_key = config.get("api_key")
|
|
412
|
+
if not api_key:
|
|
413
|
+
self.loading_dialog.hide_loading()
|
|
414
|
+
from PySide6.QtWidgets import QMessageBox
|
|
415
|
+
QMessageBox.warning(self, "Missing API Key", "Pinecone requires an API key to connect.")
|
|
416
|
+
return
|
|
417
|
+
self.connection = PineconeConnection(api_key=api_key)
|
|
418
|
+
elif provider == "qdrant":
|
|
374
419
|
if conn_type == "persistent":
|
|
375
420
|
self.connection = QdrantConnection(path=config.get("path"))
|
|
376
421
|
elif conn_type == "http":
|
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
from typing import Optional, Dict, Any
|
|
4
4
|
from PySide6.QtWidgets import (
|
|
5
5
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
6
|
-
QGroupBox, QScrollArea, QFrame
|
|
6
|
+
QGroupBox, QScrollArea, QFrame, QPushButton
|
|
7
7
|
)
|
|
8
8
|
from PySide6.QtCore import Qt, QObject
|
|
9
|
+
from PySide6.QtWidgets import QDialog
|
|
10
|
+
from PySide6.QtWidgets import QApplication
|
|
9
11
|
|
|
10
12
|
from vector_inspector.core.connections.base_connection import VectorDBConnection
|
|
11
13
|
from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
|
|
12
14
|
from vector_inspector.core.connections.qdrant_connection import QdrantConnection
|
|
15
|
+
from vector_inspector.core.connections.pinecone_connection import PineconeConnection
|
|
16
|
+
from vector_inspector.core.cache_manager import get_cache_manager
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
class InfoPanel(QWidget):
|
|
@@ -18,7 +22,10 @@ class InfoPanel(QWidget):
|
|
|
18
22
|
def __init__(self, connection: VectorDBConnection, parent=None):
|
|
19
23
|
super().__init__(parent)
|
|
20
24
|
self.connection = connection
|
|
25
|
+
self.connection_id: str = "" # Will be set when collection is set
|
|
21
26
|
self.current_collection: str = ""
|
|
27
|
+
self.current_database: str = ""
|
|
28
|
+
self.cache_manager = get_cache_manager()
|
|
22
29
|
self._setup_ui()
|
|
23
30
|
|
|
24
31
|
def _setup_ui(self):
|
|
@@ -65,10 +72,31 @@ class InfoPanel(QWidget):
|
|
|
65
72
|
self.distance_metric_label = self._create_info_row("Distance Metric:", "N/A")
|
|
66
73
|
self.total_points_label = self._create_info_row("Total Points:", "0")
|
|
67
74
|
|
|
75
|
+
# Embedding model row with configure button
|
|
76
|
+
embedding_row = QWidget()
|
|
77
|
+
embedding_layout = QHBoxLayout(embedding_row)
|
|
78
|
+
embedding_layout.setContentsMargins(0, 2, 0, 2)
|
|
79
|
+
|
|
80
|
+
embedding_label = QLabel("<b>Embedding Model:</b>")
|
|
81
|
+
embedding_label.setMinimumWidth(150)
|
|
82
|
+
self.embedding_model_label = QLabel("Auto-detect")
|
|
83
|
+
self.embedding_model_label.setStyleSheet("color: gray;")
|
|
84
|
+
self.embedding_model_label.setWordWrap(True)
|
|
85
|
+
|
|
86
|
+
self.configure_embedding_btn = QPushButton("Configure...")
|
|
87
|
+
self.configure_embedding_btn.setMaximumWidth(100)
|
|
88
|
+
self.configure_embedding_btn.clicked.connect(self._configure_embedding_model)
|
|
89
|
+
self.configure_embedding_btn.setEnabled(False)
|
|
90
|
+
|
|
91
|
+
embedding_layout.addWidget(embedding_label)
|
|
92
|
+
embedding_layout.addWidget(self.embedding_model_label, 1)
|
|
93
|
+
embedding_layout.addWidget(self.configure_embedding_btn)
|
|
94
|
+
|
|
68
95
|
collection_layout.addWidget(self.collection_name_label)
|
|
69
96
|
collection_layout.addWidget(self.vector_dim_label)
|
|
70
97
|
collection_layout.addWidget(self.distance_metric_label)
|
|
71
98
|
collection_layout.addWidget(self.total_points_label)
|
|
99
|
+
collection_layout.addWidget(embedding_row)
|
|
72
100
|
|
|
73
101
|
# Payload Schema subsection
|
|
74
102
|
schema_label = QLabel("<b>Payload Schema:</b>")
|
|
@@ -173,6 +201,15 @@ class InfoPanel(QWidget):
|
|
|
173
201
|
self._update_label(self.api_key_label, "Present (hidden)")
|
|
174
202
|
else:
|
|
175
203
|
self._update_label(self.api_key_label, "Not configured")
|
|
204
|
+
|
|
205
|
+
elif isinstance(self.connection, PineconeConnection):
|
|
206
|
+
self._update_label(self.connection_type_label, "Cloud")
|
|
207
|
+
self._update_label(self.endpoint_label, "Pinecone Cloud")
|
|
208
|
+
|
|
209
|
+
if self.connection.api_key:
|
|
210
|
+
self._update_label(self.api_key_label, "Present (hidden)")
|
|
211
|
+
else:
|
|
212
|
+
self._update_label(self.api_key_label, "Not configured")
|
|
176
213
|
else:
|
|
177
214
|
self._update_label(self.connection_type_label, "Unknown")
|
|
178
215
|
self._update_label(self.endpoint_label, "N/A")
|
|
@@ -200,7 +237,7 @@ class InfoPanel(QWidget):
|
|
|
200
237
|
return
|
|
201
238
|
|
|
202
239
|
try:
|
|
203
|
-
# Get collection info
|
|
240
|
+
# Get collection info from database
|
|
204
241
|
collection_info = self.connection.get_collection_info(self.current_collection)
|
|
205
242
|
|
|
206
243
|
if not collection_info:
|
|
@@ -212,59 +249,18 @@ class InfoPanel(QWidget):
|
|
|
212
249
|
self.provider_details_label.setText("N/A")
|
|
213
250
|
return
|
|
214
251
|
|
|
215
|
-
#
|
|
216
|
-
self.
|
|
217
|
-
|
|
218
|
-
# Vector dimension
|
|
219
|
-
vector_dim = collection_info.get("vector_dimension", "Unknown")
|
|
220
|
-
self._update_label(self.vector_dim_label, str(vector_dim))
|
|
221
|
-
|
|
222
|
-
# Distance metric
|
|
223
|
-
distance = collection_info.get("distance_metric", "Unknown")
|
|
224
|
-
self._update_label(self.distance_metric_label, distance)
|
|
225
|
-
|
|
226
|
-
# Total points
|
|
227
|
-
count = collection_info.get("count", 0)
|
|
228
|
-
self._update_label(self.total_points_label, f"{count:,}")
|
|
229
|
-
|
|
230
|
-
# Metadata schema
|
|
231
|
-
metadata_fields = collection_info.get("metadata_fields", [])
|
|
232
|
-
if metadata_fields:
|
|
233
|
-
schema_text = "\n".join([f"• {field}" for field in sorted(metadata_fields)])
|
|
234
|
-
self.schema_label.setText(schema_text)
|
|
235
|
-
self.schema_label.setStyleSheet("color: white; padding-left: 20px; font-family: monospace;")
|
|
236
|
-
else:
|
|
237
|
-
self.schema_label.setText("No metadata fields found")
|
|
238
|
-
self.schema_label.setStyleSheet("color: gray; padding-left: 20px;")
|
|
239
|
-
|
|
240
|
-
# Provider-specific details
|
|
241
|
-
details_list = []
|
|
242
|
-
|
|
243
|
-
if isinstance(self.connection, ChromaDBConnection):
|
|
244
|
-
details_list.append("• Provider: ChromaDB")
|
|
245
|
-
details_list.append("• Supports: Documents, Metadata, Embeddings")
|
|
246
|
-
details_list.append("• Default embedding: all-MiniLM-L6-v2")
|
|
247
|
-
|
|
248
|
-
elif isinstance(self.connection, QdrantConnection):
|
|
249
|
-
details_list.append("• Provider: Qdrant")
|
|
250
|
-
details_list.append("• Supports: Points, Payload, Vectors")
|
|
251
|
-
# Get additional Qdrant-specific info if available
|
|
252
|
-
if "config" in collection_info:
|
|
253
|
-
config = collection_info["config"]
|
|
254
|
-
if "hnsw_config" in config:
|
|
255
|
-
hnsw = config["hnsw_config"]
|
|
256
|
-
details_list.append(f"• HNSW M: {hnsw.get('m', 'N/A')}")
|
|
257
|
-
details_list.append(f"• HNSW ef_construct: {hnsw.get('ef_construct', 'N/A')}")
|
|
258
|
-
if "optimizer_config" in config:
|
|
259
|
-
opt = config["optimizer_config"]
|
|
260
|
-
details_list.append(f"• Indexing threshold: {opt.get('indexing_threshold', 'N/A')}")
|
|
252
|
+
# Display the info
|
|
253
|
+
self._display_collection_info(collection_info)
|
|
261
254
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
255
|
+
# Save to cache
|
|
256
|
+
if self.current_database and self.current_collection:
|
|
257
|
+
print(f"[InfoPanel] Saving collection info to cache: db='{self.current_database}', coll='{self.current_collection}'")
|
|
258
|
+
self.cache_manager.update(
|
|
259
|
+
self.current_database,
|
|
260
|
+
self.current_collection,
|
|
261
|
+
user_inputs={'collection_info': collection_info}
|
|
262
|
+
)
|
|
263
|
+
print(f"[InfoPanel] ✓ Saved collection info to cache.")
|
|
268
264
|
|
|
269
265
|
except Exception as e:
|
|
270
266
|
self._update_label(self.collection_name_label, self.current_collection)
|
|
@@ -275,9 +271,101 @@ class InfoPanel(QWidget):
|
|
|
275
271
|
self.schema_label.setStyleSheet("color: red; padding-left: 20px;")
|
|
276
272
|
self.provider_details_label.setText("N/A")
|
|
277
273
|
|
|
278
|
-
def
|
|
274
|
+
def _display_collection_info(self, collection_info: Dict[str, Any]):
|
|
275
|
+
"""Display collection information (from cache or fresh query)."""
|
|
276
|
+
# Update basic info
|
|
277
|
+
self._update_label(self.collection_name_label, self.current_collection)
|
|
278
|
+
|
|
279
|
+
# Vector dimension
|
|
280
|
+
vector_dim = collection_info.get("vector_dimension", "Unknown")
|
|
281
|
+
self._update_label(self.vector_dim_label, str(vector_dim))
|
|
282
|
+
|
|
283
|
+
# Enable configure button if we have a valid dimension
|
|
284
|
+
self.configure_embedding_btn.setEnabled(
|
|
285
|
+
vector_dim != "Unknown" and isinstance(vector_dim, int)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Update embedding model display
|
|
289
|
+
self._update_embedding_model_display(collection_info)
|
|
290
|
+
|
|
291
|
+
# Distance metric
|
|
292
|
+
distance = collection_info.get("distance_metric", "Unknown")
|
|
293
|
+
self._update_label(self.distance_metric_label, distance)
|
|
294
|
+
|
|
295
|
+
# Total points
|
|
296
|
+
count = collection_info.get("count", 0)
|
|
297
|
+
self._update_label(self.total_points_label, f"{count:,}")
|
|
298
|
+
|
|
299
|
+
# Metadata schema
|
|
300
|
+
metadata_fields = collection_info.get("metadata_fields", [])
|
|
301
|
+
if metadata_fields:
|
|
302
|
+
schema_text = "\n".join([f"• {field}" for field in sorted(metadata_fields)])
|
|
303
|
+
self.schema_label.setText(schema_text)
|
|
304
|
+
self.schema_label.setStyleSheet("color: white; padding-left: 20px; font-family: monospace;")
|
|
305
|
+
else:
|
|
306
|
+
self.schema_label.setText("No metadata fields found")
|
|
307
|
+
self.schema_label.setStyleSheet("color: gray; padding-left: 20px;")
|
|
308
|
+
|
|
309
|
+
# Provider-specific details
|
|
310
|
+
details_list = []
|
|
311
|
+
|
|
312
|
+
if isinstance(self.connection, ChromaDBConnection):
|
|
313
|
+
details_list.append("• Provider: ChromaDB")
|
|
314
|
+
details_list.append("• Supports: Documents, Metadata, Embeddings")
|
|
315
|
+
details_list.append("• Default embedding: all-MiniLM-L6-v2")
|
|
316
|
+
|
|
317
|
+
elif isinstance(self.connection, QdrantConnection):
|
|
318
|
+
details_list.append("• Provider: Qdrant")
|
|
319
|
+
details_list.append("• Supports: Points, Payload, Vectors")
|
|
320
|
+
# Get additional Qdrant-specific info if available
|
|
321
|
+
if "config" in collection_info:
|
|
322
|
+
config = collection_info["config"]
|
|
323
|
+
if "hnsw_config" in config:
|
|
324
|
+
hnsw = config["hnsw_config"]
|
|
325
|
+
details_list.append(f"• HNSW M: {hnsw.get('m', 'N/A')}")
|
|
326
|
+
details_list.append(f"• HNSW ef_construct: {hnsw.get('ef_construct', 'N/A')}")
|
|
327
|
+
if "optimizer_config" in config:
|
|
328
|
+
opt = config["optimizer_config"]
|
|
329
|
+
details_list.append(f"• Indexing threshold: {opt.get('indexing_threshold', 'N/A')}")
|
|
330
|
+
|
|
331
|
+
elif isinstance(self.connection, PineconeConnection):
|
|
332
|
+
details_list.append("• Provider: Pinecone")
|
|
333
|
+
details_list.append("• Supports: Vectors, Metadata")
|
|
334
|
+
details_list.append("• Cloud-hosted vector database")
|
|
335
|
+
# Add Pinecone-specific info if available
|
|
336
|
+
if "host" in collection_info:
|
|
337
|
+
details_list.append(f"• Host: {collection_info['host']}")
|
|
338
|
+
if "status" in collection_info:
|
|
339
|
+
details_list.append(f"• Status: {collection_info['status']}")
|
|
340
|
+
if "spec" in collection_info:
|
|
341
|
+
details_list.append(f"• Spec: {collection_info['spec']}")
|
|
342
|
+
|
|
343
|
+
if details_list:
|
|
344
|
+
self.provider_details_label.setText("\n".join(details_list))
|
|
345
|
+
self.provider_details_label.setStyleSheet("color: white; padding-left: 20px; font-family: monospace;")
|
|
346
|
+
else:
|
|
347
|
+
self.provider_details_label.setText("No additional details available")
|
|
348
|
+
self.provider_details_label.setStyleSheet("color: gray; padding-left: 20px;")
|
|
349
|
+
|
|
350
|
+
def set_collection(self, collection_name: str, database_name: str = ""):
|
|
279
351
|
"""Set the current collection and refresh its information."""
|
|
280
352
|
self.current_collection = collection_name
|
|
353
|
+
# Always update database_name if provided
|
|
354
|
+
if database_name:
|
|
355
|
+
self.current_database = database_name
|
|
356
|
+
self.connection_id = database_name # database_name is the connection ID
|
|
357
|
+
|
|
358
|
+
print(f"[InfoPanel] Setting collection: db='{self.current_database}', coll='{collection_name}'")
|
|
359
|
+
|
|
360
|
+
# Check cache first for collection info
|
|
361
|
+
cached = self.cache_manager.get(self.current_database, self.current_collection)
|
|
362
|
+
if cached and hasattr(cached, 'user_inputs') and cached.user_inputs.get('collection_info'):
|
|
363
|
+
print(f"[InfoPanel] ✓ Cache HIT! Loading collection info from cache.")
|
|
364
|
+
collection_info = cached.user_inputs['collection_info']
|
|
365
|
+
self._display_collection_info(collection_info)
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
print(f"[InfoPanel] ✗ Cache MISS. Loading collection info from database...")
|
|
281
369
|
self.refresh_collection_info()
|
|
282
370
|
|
|
283
371
|
def _update_label(self, row_widget: QWidget, value: str):
|
|
@@ -285,3 +373,132 @@ class InfoPanel(QWidget):
|
|
|
285
373
|
value_label = row_widget.property("value_label")
|
|
286
374
|
if value_label and isinstance(value_label, QLabel):
|
|
287
375
|
value_label.setText(value)
|
|
376
|
+
|
|
377
|
+
def _update_embedding_model_display(self, collection_info: Dict[str, Any]):
|
|
378
|
+
"""Update the embedding model label based on current configuration."""
|
|
379
|
+
from ...services.settings_service import SettingsService
|
|
380
|
+
|
|
381
|
+
# Check if stored in collection metadata
|
|
382
|
+
if 'embedding_model' in collection_info:
|
|
383
|
+
model_name = collection_info['embedding_model']
|
|
384
|
+
model_type = collection_info.get('embedding_model_type', 'stored')
|
|
385
|
+
self.embedding_model_label.setText(f"{model_name} ({model_type})")
|
|
386
|
+
self.embedding_model_label.setStyleSheet("color: lightgreen;")
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
# Try to get from connection using the helper method
|
|
390
|
+
if self.connection and self.current_collection:
|
|
391
|
+
detected_model = self.connection.get_embedding_model(self.current_collection, self.connection_id)
|
|
392
|
+
if detected_model:
|
|
393
|
+
self.embedding_model_label.setText(f"{detected_model} (detected)")
|
|
394
|
+
self.embedding_model_label.setStyleSheet("color: lightgreen;")
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
# Check user settings
|
|
398
|
+
settings = SettingsService()
|
|
399
|
+
model_info = settings.get_embedding_model(self.connection_id, self.current_collection)
|
|
400
|
+
|
|
401
|
+
if model_info:
|
|
402
|
+
model_name = model_info['model']
|
|
403
|
+
model_type = model_info.get('type', 'unknown')
|
|
404
|
+
self.embedding_model_label.setText(f"{model_name} ({model_type})")
|
|
405
|
+
self.embedding_model_label.setStyleSheet("color: lightblue;")
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
# No configuration - using auto-detect
|
|
409
|
+
self.embedding_model_label.setText("Auto-detect (dimension-based)")
|
|
410
|
+
self.embedding_model_label.setStyleSheet("color: orange;")
|
|
411
|
+
|
|
412
|
+
def _configure_embedding_model(self):
|
|
413
|
+
"""Open dialog to configure embedding model for current collection."""
|
|
414
|
+
if not self.current_collection:
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
# Show loading immediately; preparing can touch DB/registry
|
|
418
|
+
from ..components.loading_dialog import LoadingDialog
|
|
419
|
+
loading = LoadingDialog("Preparing model configuration...", self)
|
|
420
|
+
loading.show_loading("Preparing model configuration...")
|
|
421
|
+
QApplication.processEvents()
|
|
422
|
+
|
|
423
|
+
from ..dialogs import ProviderTypeDialog, EmbeddingConfigDialog
|
|
424
|
+
from ...services.settings_service import SettingsService
|
|
425
|
+
|
|
426
|
+
# Get current collection info
|
|
427
|
+
try:
|
|
428
|
+
collection_info = self.connection.get_collection_info(self.current_collection)
|
|
429
|
+
finally:
|
|
430
|
+
# Hide loading before presenting dialogs
|
|
431
|
+
loading.hide_loading()
|
|
432
|
+
if not collection_info:
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
vector_dim = collection_info.get("vector_dimension")
|
|
436
|
+
if not vector_dim or vector_dim == "Unknown":
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
# Get current configuration if any
|
|
440
|
+
settings = SettingsService()
|
|
441
|
+
|
|
442
|
+
current_model = None
|
|
443
|
+
current_type = None
|
|
444
|
+
|
|
445
|
+
# Check metadata first
|
|
446
|
+
if 'embedding_model' in collection_info:
|
|
447
|
+
current_model = collection_info['embedding_model']
|
|
448
|
+
current_type = collection_info.get('embedding_model_type', 'stored')
|
|
449
|
+
# Then check settings
|
|
450
|
+
else:
|
|
451
|
+
model_info = settings.get_embedding_model(self.connection_id, self.current_collection)
|
|
452
|
+
if model_info:
|
|
453
|
+
current_model = model_info.get('model')
|
|
454
|
+
current_type = model_info.get('type')
|
|
455
|
+
|
|
456
|
+
# Step 1: Provider Type Selection
|
|
457
|
+
type_dialog = ProviderTypeDialog(
|
|
458
|
+
self.current_collection,
|
|
459
|
+
vector_dim,
|
|
460
|
+
self
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
type_result = type_dialog.exec()
|
|
464
|
+
if type_result != QDialog.DialogCode.Accepted:
|
|
465
|
+
return # User cancelled
|
|
466
|
+
|
|
467
|
+
provider_type = type_dialog.get_selected_type()
|
|
468
|
+
if not provider_type:
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
# Step 2: Model Selection (filtered by provider type)
|
|
472
|
+
model_dialog = EmbeddingConfigDialog(
|
|
473
|
+
self.current_collection,
|
|
474
|
+
vector_dim,
|
|
475
|
+
provider_type,
|
|
476
|
+
current_model,
|
|
477
|
+
current_type,
|
|
478
|
+
self
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# Optionally show brief loading while populating models
|
|
482
|
+
# (dialog itself handles content; only show if provider lists are large)
|
|
483
|
+
result = model_dialog.exec()
|
|
484
|
+
|
|
485
|
+
if result == QDialog.DialogCode.Accepted:
|
|
486
|
+
# Save the configuration using the new SettingsService method
|
|
487
|
+
selection = model_dialog.get_selection()
|
|
488
|
+
if selection:
|
|
489
|
+
model_name, model_type = selection
|
|
490
|
+
settings.save_embedding_model(self.connection_id, self.current_collection, model_name, model_type)
|
|
491
|
+
|
|
492
|
+
# Refresh display
|
|
493
|
+
self._update_embedding_model_display(collection_info)
|
|
494
|
+
|
|
495
|
+
print(f"✓ Configured embedding model for '{self.current_collection}': {model_name} ({model_type})")
|
|
496
|
+
|
|
497
|
+
elif result == 2: # Clear configuration
|
|
498
|
+
# Remove from settings using the new SettingsService method
|
|
499
|
+
settings.remove_embedding_model(self.connection_id, self.current_collection)
|
|
500
|
+
|
|
501
|
+
# Refresh display
|
|
502
|
+
self._update_embedding_model_display(collection_info)
|
|
503
|
+
|
|
504
|
+
print(f"✓ Cleared embedding model configuration for '{self.current_collection}'")
|
|
@@ -16,6 +16,7 @@ from vector_inspector.ui.components.filter_builder import FilterBuilder
|
|
|
16
16
|
from vector_inspector.services.import_export_service import ImportExportService
|
|
17
17
|
from vector_inspector.services.filter_service import apply_client_side_filters
|
|
18
18
|
from vector_inspector.services.settings_service import SettingsService
|
|
19
|
+
from vector_inspector.core.cache_manager import get_cache_manager, CacheEntry
|
|
19
20
|
from PySide6.QtWidgets import QApplication
|
|
20
21
|
|
|
21
22
|
|
|
@@ -57,12 +58,14 @@ class MetadataView(QWidget):
|
|
|
57
58
|
super().__init__(parent)
|
|
58
59
|
self.connection = connection
|
|
59
60
|
self.current_collection: str = ""
|
|
61
|
+
self.current_database: str = ""
|
|
60
62
|
self.current_data: Optional[Dict[str, Any]] = None
|
|
61
63
|
self.page_size = 50
|
|
62
64
|
self.current_page = 0
|
|
63
65
|
self.loading_dialog = LoadingDialog("Loading data...", self)
|
|
64
66
|
self.settings_service = SettingsService()
|
|
65
67
|
self.load_thread: Optional[DataLoadThread] = None
|
|
68
|
+
self.cache_manager = get_cache_manager()
|
|
66
69
|
|
|
67
70
|
# Debounce timer for filter changes
|
|
68
71
|
self.filter_reload_timer = QTimer()
|
|
@@ -107,8 +110,9 @@ class MetadataView(QWidget):
|
|
|
107
110
|
controls_layout.addStretch()
|
|
108
111
|
|
|
109
112
|
# Refresh button
|
|
110
|
-
self.refresh_button = QPushButton("Refresh")
|
|
111
|
-
self.refresh_button.clicked.connect(self.
|
|
113
|
+
self.refresh_button = QPushButton("🔄 Refresh")
|
|
114
|
+
self.refresh_button.clicked.connect(self._refresh_data)
|
|
115
|
+
self.refresh_button.setToolTip("Refresh data and clear cache")
|
|
112
116
|
controls_layout.addWidget(self.refresh_button)
|
|
113
117
|
|
|
114
118
|
# Add/Delete buttons
|
|
@@ -172,9 +176,40 @@ class MetadataView(QWidget):
|
|
|
172
176
|
self.status_label.setStyleSheet("color: gray;")
|
|
173
177
|
layout.addWidget(self.status_label)
|
|
174
178
|
|
|
175
|
-
def set_collection(self, collection_name: str):
|
|
179
|
+
def set_collection(self, collection_name: str, database_name: str = ""):
|
|
176
180
|
"""Set the current collection to display."""
|
|
177
181
|
self.current_collection = collection_name
|
|
182
|
+
# Always update database_name if provided (even if empty string on first call)
|
|
183
|
+
if database_name: # Only update if non-empty
|
|
184
|
+
self.current_database = database_name
|
|
185
|
+
|
|
186
|
+
# Debug: Check cache status
|
|
187
|
+
print(f"[MetadataView] Setting collection: db='{self.current_database}', coll='{collection_name}'")
|
|
188
|
+
print(f"[MetadataView] Cache enabled: {self.cache_manager.is_enabled()}")
|
|
189
|
+
|
|
190
|
+
# Check cache first
|
|
191
|
+
cached = self.cache_manager.get(self.current_database, self.current_collection)
|
|
192
|
+
if cached and cached.data:
|
|
193
|
+
print(f"[MetadataView] ✓ Cache HIT! Loading from cache.")
|
|
194
|
+
# Restore from cache
|
|
195
|
+
self.current_page = 0
|
|
196
|
+
self.current_data = cached.data
|
|
197
|
+
self._populate_table(cached.data)
|
|
198
|
+
self._update_pagination_controls()
|
|
199
|
+
self._update_filter_fields(cached.data)
|
|
200
|
+
|
|
201
|
+
# Restore UI state
|
|
202
|
+
if cached.scroll_position:
|
|
203
|
+
self.table.verticalScrollBar().setValue(cached.scroll_position)
|
|
204
|
+
if cached.search_query:
|
|
205
|
+
# Restore filter state if applicable
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
self.status_label.setText(f"✓ Loaded from cache - {len(cached.data.get('ids', []))} items")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
print(f"[MetadataView] ✗ Cache MISS. Loading from database...")
|
|
212
|
+
# Not in cache, load from database
|
|
178
213
|
self.current_page = 0
|
|
179
214
|
|
|
180
215
|
# Update filter builder with supported operators
|
|
@@ -246,6 +281,19 @@ class MetadataView(QWidget):
|
|
|
246
281
|
|
|
247
282
|
# Update filter builder with available metadata fields
|
|
248
283
|
self._update_filter_fields(data)
|
|
284
|
+
|
|
285
|
+
# Save to cache
|
|
286
|
+
if self.current_database and self.current_collection:
|
|
287
|
+
print(f"[MetadataView] Saving to cache: db='{self.current_database}', coll='{self.current_collection}'")
|
|
288
|
+
cache_entry = CacheEntry(
|
|
289
|
+
data=data,
|
|
290
|
+
scroll_position=self.table.verticalScrollBar().value(),
|
|
291
|
+
search_query=self.filter_builder.to_dict() if hasattr(self.filter_builder, 'to_dict') else ""
|
|
292
|
+
)
|
|
293
|
+
self.cache_manager.set(self.current_database, self.current_collection, cache_entry)
|
|
294
|
+
print(f"[MetadataView] ✓ Saved to cache. Total entries: {len(self.cache_manager._cache)}")
|
|
295
|
+
else:
|
|
296
|
+
print(f"[MetadataView] ✗ NOT saving to cache - db='{self.current_database}', coll='{self.current_collection}'")
|
|
249
297
|
|
|
250
298
|
def _on_load_error(self, error_msg: str):
|
|
251
299
|
"""Handle error from background thread."""
|
|
@@ -365,6 +413,9 @@ class MetadataView(QWidget):
|
|
|
365
413
|
)
|
|
366
414
|
|
|
367
415
|
if success:
|
|
416
|
+
# Invalidate cache after adding item
|
|
417
|
+
if self.current_database and self.current_collection:
|
|
418
|
+
self.cache_manager.invalidate(self.current_database, self.current_collection)
|
|
368
419
|
QMessageBox.information(self, "Success", "Item added successfully.")
|
|
369
420
|
self._load_data()
|
|
370
421
|
else:
|
|
@@ -399,6 +450,9 @@ class MetadataView(QWidget):
|
|
|
399
450
|
if reply == QMessageBox.Yes:
|
|
400
451
|
success = self.connection.delete_items(self.current_collection, ids=ids_to_delete)
|
|
401
452
|
if success:
|
|
453
|
+
# Invalidate cache after deletion
|
|
454
|
+
if self.current_database and self.current_collection:
|
|
455
|
+
self.cache_manager.invalidate(self.current_database, self.current_collection)
|
|
402
456
|
QMessageBox.information(self, "Success", "Items deleted successfully.")
|
|
403
457
|
self._load_data()
|
|
404
458
|
else:
|
|
@@ -422,6 +476,13 @@ class MetadataView(QWidget):
|
|
|
422
476
|
self.current_page = 0
|
|
423
477
|
self._load_data()
|
|
424
478
|
|
|
479
|
+
def _refresh_data(self):
|
|
480
|
+
"""Refresh data and invalidate cache."""
|
|
481
|
+
if self.current_database and self.current_collection:
|
|
482
|
+
self.cache_manager.invalidate(self.current_database, self.current_collection)
|
|
483
|
+
self.current_page = 0
|
|
484
|
+
self._load_data()
|
|
485
|
+
|
|
425
486
|
def _on_row_double_clicked(self, index):
|
|
426
487
|
"""Handle double-click on a row to edit item."""
|
|
427
488
|
if not self.current_collection or not self.current_data:
|
|
@@ -462,6 +523,9 @@ class MetadataView(QWidget):
|
|
|
462
523
|
)
|
|
463
524
|
|
|
464
525
|
if success:
|
|
526
|
+
# Invalidate cache after updating item
|
|
527
|
+
if self.current_database and self.current_collection:
|
|
528
|
+
self.cache_manager.invalidate(self.current_database, self.current_collection)
|
|
465
529
|
QMessageBox.information(self, "Success", "Item updated successfully.")
|
|
466
530
|
self._load_data()
|
|
467
531
|
else:
|
|
@@ -653,6 +717,10 @@ class MetadataView(QWidget):
|
|
|
653
717
|
self.loading_dialog.hide_loading()
|
|
654
718
|
|
|
655
719
|
if success:
|
|
720
|
+
# Invalidate cache after import
|
|
721
|
+
if self.current_database and self.current_collection:
|
|
722
|
+
self.cache_manager.invalidate(self.current_database, self.current_collection)
|
|
723
|
+
|
|
656
724
|
# Save the directory for next time
|
|
657
725
|
from pathlib import Path
|
|
658
726
|
self.settings_service.set("last_import_export_dir", str(Path(file_path).parent))
|