vector-inspector 0.2.5__py3-none-any.whl → 0.2.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. vector_inspector/core/cache_manager.py +159 -0
  2. vector_inspector/core/connection_manager.py +277 -0
  3. vector_inspector/core/connections/chroma_connection.py +90 -5
  4. vector_inspector/core/connections/qdrant_connection.py +62 -8
  5. vector_inspector/core/embedding_utils.py +140 -0
  6. vector_inspector/services/backup_restore_service.py +3 -29
  7. vector_inspector/services/credential_service.py +130 -0
  8. vector_inspector/services/filter_service.py +1 -1
  9. vector_inspector/services/profile_service.py +409 -0
  10. vector_inspector/services/settings_service.py +20 -1
  11. vector_inspector/services/visualization_service.py +11 -7
  12. vector_inspector/ui/components/connection_manager_panel.py +320 -0
  13. vector_inspector/ui/components/profile_manager_panel.py +518 -0
  14. vector_inspector/ui/dialogs/__init__.py +5 -0
  15. vector_inspector/ui/dialogs/cross_db_migration.py +364 -0
  16. vector_inspector/ui/dialogs/embedding_config_dialog.py +176 -0
  17. vector_inspector/ui/main_window.py +429 -181
  18. vector_inspector/ui/views/connection_view.py +43 -8
  19. vector_inspector/ui/views/info_panel.py +226 -80
  20. vector_inspector/ui/views/metadata_view.py +136 -28
  21. vector_inspector/ui/views/search_view.py +43 -3
  22. {vector_inspector-0.2.5.dist-info → vector_inspector-0.2.7.dist-info}/METADATA +5 -3
  23. vector_inspector-0.2.7.dist-info/RECORD +45 -0
  24. vector_inspector-0.2.5.dist-info/RECORD +0 -35
  25. {vector_inspector-0.2.5.dist-info → vector_inspector-0.2.7.dist-info}/WHEEL +0 -0
  26. {vector_inspector-0.2.5.dist-info → vector_inspector-0.2.7.dist-info}/entry_points.txt +0 -0
@@ -5,7 +5,7 @@ from PySide6.QtWidgets import (
5
5
  QPushButton, QDialog, QFormLayout, QLineEdit,
6
6
  QRadioButton, QButtonGroup, QGroupBox, QFileDialog, QComboBox, QApplication, QCheckBox
7
7
  )
8
- from PySide6.QtCore import Signal
8
+ from PySide6.QtCore import Signal, QThread
9
9
 
10
10
  from vector_inspector.core.connections.base_connection import VectorDBConnection
11
11
  from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
@@ -14,6 +14,28 @@ from vector_inspector.ui.components.loading_dialog import LoadingDialog
14
14
  from vector_inspector.services.settings_service import SettingsService
15
15
 
16
16
 
17
+ class ConnectionThread(QThread):
18
+ """Background thread for connecting to database."""
19
+
20
+ finished = Signal(bool, list) # success, collections
21
+
22
+ def __init__(self, connection):
23
+ super().__init__()
24
+ self.connection = connection
25
+
26
+ def run(self):
27
+ """Connect to database and get collections."""
28
+ try:
29
+ success = self.connection.connect()
30
+ if success:
31
+ collections = self.connection.list_collections()
32
+ self.finished.emit(True, collections)
33
+ else:
34
+ self.finished.emit(False, [])
35
+ except Exception:
36
+ self.finished.emit(False, [])
37
+
38
+
17
39
  class ConnectionDialog(QDialog):
18
40
  """Dialog for configuring database connection."""
19
41
 
@@ -175,6 +197,9 @@ class ConnectionDialog(QDialog):
175
197
 
176
198
  def get_connection_config(self):
177
199
  """Get connection configuration from dialog."""
200
+ # Get current provider from combo box to ensure it's up to date
201
+ self.provider = self.provider_combo.currentData()
202
+
178
203
  config = {"provider": self.provider}
179
204
 
180
205
  if self.persistent_radio.isChecked():
@@ -294,6 +319,7 @@ class ConnectionView(QWidget):
294
319
  self.connection = connection
295
320
  self.loading_dialog = LoadingDialog("Connecting to database...", self)
296
321
  self.settings_service = SettingsService()
322
+ self.connection_thread = None
297
323
  self._setup_ui()
298
324
 
299
325
  # Try to auto-connect if enabled in settings
@@ -339,7 +365,6 @@ class ConnectionView(QWidget):
339
365
  def _connect_with_config(self, config: dict):
340
366
  """Connect to database with given configuration."""
341
367
  self.loading_dialog.show_loading("Connecting to database...")
342
- QApplication.processEvents()
343
368
 
344
369
  provider = config.get("provider", "chromadb")
345
370
  conn_type = config.get("type")
@@ -367,11 +392,25 @@ class ConnectionView(QWidget):
367
392
  else: # ephemeral
368
393
  self.connection = ChromaDBConnection()
369
394
 
395
+ # Store config for later use
396
+ self._pending_config = config
397
+
370
398
  # Notify parent that connection instance changed
371
399
  self.connection_created.emit(self.connection)
372
- success = self.connection.connect()
373
-
400
+
401
+ # Start background thread to connect
402
+ self.connection_thread = ConnectionThread(self.connection)
403
+ self.connection_thread.finished.connect(self._on_connection_finished)
404
+ self.connection_thread.start()
405
+
406
+ def _on_connection_finished(self, success: bool, collections: list):
407
+ """Handle connection thread completion."""
408
+ self.loading_dialog.hide_loading()
409
+
374
410
  if success:
411
+ config = self._pending_config
412
+ provider = config.get("provider", "chromadb")
413
+
375
414
  # Show provider, path/host + collection count for clarity
376
415
  details = []
377
416
  details.append(f"provider: {provider}")
@@ -380,7 +419,6 @@ class ConnectionView(QWidget):
380
419
  if hasattr(self.connection, 'host') and self.connection.host:
381
420
  port = getattr(self.connection, 'port', None)
382
421
  details.append(f"host: {self.connection.host}:{port}")
383
- collections = self.connection.list_collections()
384
422
  count_text = f"collections: {len(collections)}"
385
423
  info = ", ".join(details)
386
424
  self.status_label.setText(f"Status: Connected ({info}, {count_text})")
@@ -401,9 +439,6 @@ class ConnectionView(QWidget):
401
439
  self.disconnect_button.setEnabled(False)
402
440
  self.connection_changed.emit(False)
403
441
 
404
- # Close loading dialog after everything is complete
405
- self.loading_dialog.hide_loading()
406
-
407
442
  def _disconnect(self):
408
443
  """Disconnect from database."""
409
444
  self.connection.disconnect()
@@ -3,13 +3,15 @@
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
9
10
 
10
11
  from vector_inspector.core.connections.base_connection import VectorDBConnection
11
12
  from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
12
13
  from vector_inspector.core.connections.qdrant_connection import QdrantConnection
14
+ from vector_inspector.core.cache_manager import get_cache_manager
13
15
 
14
16
 
15
17
  class InfoPanel(QWidget):
@@ -18,7 +20,10 @@ class InfoPanel(QWidget):
18
20
  def __init__(self, connection: VectorDBConnection, parent=None):
19
21
  super().__init__(parent)
20
22
  self.connection = connection
23
+ self.connection_id: str = "" # Will be set when collection is set
21
24
  self.current_collection: str = ""
25
+ self.current_database: str = ""
26
+ self.cache_manager = get_cache_manager()
22
27
  self._setup_ui()
23
28
 
24
29
  def _setup_ui(self):
@@ -56,18 +61,6 @@ class InfoPanel(QWidget):
56
61
  self.db_group.setLayout(db_layout)
57
62
  container_layout.addWidget(self.db_group)
58
63
 
59
- # Collections List Section
60
- self.collections_group = QGroupBox("Available Collections")
61
- collections_layout = QVBoxLayout()
62
-
63
- self.collections_list_label = QLabel("No collections")
64
- self.collections_list_label.setWordWrap(True)
65
- self.collections_list_label.setStyleSheet("color: gray; padding: 10px;")
66
- collections_layout.addWidget(self.collections_list_label)
67
-
68
- self.collections_group.setLayout(collections_layout)
69
- container_layout.addWidget(self.collections_group)
70
-
71
64
  # Collection Information Section
72
65
  self.collection_group = QGroupBox("Collection Information")
73
66
  collection_layout = QVBoxLayout()
@@ -77,10 +70,31 @@ class InfoPanel(QWidget):
77
70
  self.distance_metric_label = self._create_info_row("Distance Metric:", "N/A")
78
71
  self.total_points_label = self._create_info_row("Total Points:", "0")
79
72
 
73
+ # Embedding model row with configure button
74
+ embedding_row = QWidget()
75
+ embedding_layout = QHBoxLayout(embedding_row)
76
+ embedding_layout.setContentsMargins(0, 2, 0, 2)
77
+
78
+ embedding_label = QLabel("<b>Embedding Model:</b>")
79
+ embedding_label.setMinimumWidth(150)
80
+ self.embedding_model_label = QLabel("Auto-detect")
81
+ self.embedding_model_label.setStyleSheet("color: gray;")
82
+ self.embedding_model_label.setWordWrap(True)
83
+
84
+ self.configure_embedding_btn = QPushButton("Configure...")
85
+ self.configure_embedding_btn.setMaximumWidth(100)
86
+ self.configure_embedding_btn.clicked.connect(self._configure_embedding_model)
87
+ self.configure_embedding_btn.setEnabled(False)
88
+
89
+ embedding_layout.addWidget(embedding_label)
90
+ embedding_layout.addWidget(self.embedding_model_label, 1)
91
+ embedding_layout.addWidget(self.configure_embedding_btn)
92
+
80
93
  collection_layout.addWidget(self.collection_name_label)
81
94
  collection_layout.addWidget(self.vector_dim_label)
82
95
  collection_layout.addWidget(self.distance_metric_label)
83
96
  collection_layout.addWidget(self.total_points_label)
97
+ collection_layout.addWidget(embedding_row)
84
98
 
85
99
  # Payload Schema subsection
86
100
  schema_label = QLabel("<b>Payload Schema:</b>")
@@ -141,8 +155,6 @@ class InfoPanel(QWidget):
141
155
  self._update_label(self.api_key_label, "N/A")
142
156
  self._update_label(self.status_label, "Disconnected")
143
157
  self._update_label(self.collections_count_label, "0")
144
- self.collections_list_label.setText("No collections")
145
- self.collections_list_label.setStyleSheet("color: gray; padding: 10px;")
146
158
  # Also clear collection info
147
159
  self._update_label(self.collection_name_label, "No collection selected")
148
160
  self._update_label(self.vector_dim_label, "N/A")
@@ -195,22 +207,12 @@ class InfoPanel(QWidget):
195
207
  # Status
196
208
  self._update_label(self.status_label, "Connected" if self.connection.is_connected else "Disconnected")
197
209
 
198
- # List collections
210
+ # Count collections
199
211
  try:
200
212
  collections = self.connection.list_collections()
201
213
  self._update_label(self.collections_count_label, str(len(collections)))
202
-
203
- if collections:
204
- collections_text = "\n".join([f"• {name}" for name in sorted(collections)])
205
- self.collections_list_label.setText(collections_text)
206
- self.collections_list_label.setStyleSheet("color: white; padding: 10px; font-family: monospace;")
207
- else:
208
- self.collections_list_label.setText("No collections found")
209
- self.collections_list_label.setStyleSheet("color: gray; padding: 10px;")
210
214
  except Exception as e:
211
215
  self._update_label(self.collections_count_label, "Error")
212
- self.collections_list_label.setText(f"Error loading collections: {str(e)}")
213
- self.collections_list_label.setStyleSheet("color: red; padding: 10px;")
214
216
 
215
217
  def refresh_collection_info(self):
216
218
  """Refresh collection-specific information."""
@@ -224,7 +226,7 @@ class InfoPanel(QWidget):
224
226
  return
225
227
 
226
228
  try:
227
- # Get collection info
229
+ # Get collection info from database
228
230
  collection_info = self.connection.get_collection_info(self.current_collection)
229
231
 
230
232
  if not collection_info:
@@ -236,59 +238,18 @@ class InfoPanel(QWidget):
236
238
  self.provider_details_label.setText("N/A")
237
239
  return
238
240
 
239
- # Update basic info
240
- self._update_label(self.collection_name_label, self.current_collection)
241
-
242
- # Vector dimension
243
- vector_dim = collection_info.get("vector_dimension", "Unknown")
244
- self._update_label(self.vector_dim_label, str(vector_dim))
245
-
246
- # Distance metric
247
- distance = collection_info.get("distance_metric", "Unknown")
248
- self._update_label(self.distance_metric_label, distance)
249
-
250
- # Total points
251
- count = collection_info.get("count", 0)
252
- self._update_label(self.total_points_label, f"{count:,}")
253
-
254
- # Metadata schema
255
- metadata_fields = collection_info.get("metadata_fields", [])
256
- if metadata_fields:
257
- schema_text = "\n".join([f"• {field}" for field in sorted(metadata_fields)])
258
- self.schema_label.setText(schema_text)
259
- self.schema_label.setStyleSheet("color: white; padding-left: 20px; font-family: monospace;")
260
- else:
261
- self.schema_label.setText("No metadata fields found")
262
- self.schema_label.setStyleSheet("color: gray; padding-left: 20px;")
263
-
264
- # Provider-specific details
265
- details_list = []
266
-
267
- if isinstance(self.connection, ChromaDBConnection):
268
- details_list.append("• Provider: ChromaDB")
269
- details_list.append("• Supports: Documents, Metadata, Embeddings")
270
- details_list.append("• Default embedding: all-MiniLM-L6-v2")
271
-
272
- elif isinstance(self.connection, QdrantConnection):
273
- details_list.append("• Provider: Qdrant")
274
- details_list.append("• Supports: Points, Payload, Vectors")
275
- # Get additional Qdrant-specific info if available
276
- if "config" in collection_info:
277
- config = collection_info["config"]
278
- if "hnsw_config" in config:
279
- hnsw = config["hnsw_config"]
280
- details_list.append(f"• HNSW M: {hnsw.get('m', 'N/A')}")
281
- details_list.append(f"• HNSW ef_construct: {hnsw.get('ef_construct', 'N/A')}")
282
- if "optimizer_config" in config:
283
- opt = config["optimizer_config"]
284
- details_list.append(f"• Indexing threshold: {opt.get('indexing_threshold', 'N/A')}")
241
+ # Display the info
242
+ self._display_collection_info(collection_info)
285
243
 
286
- if details_list:
287
- self.provider_details_label.setText("\n".join(details_list))
288
- self.provider_details_label.setStyleSheet("color: white; padding-left: 20px; font-family: monospace;")
289
- else:
290
- self.provider_details_label.setText("No additional details available")
291
- self.provider_details_label.setStyleSheet("color: gray; padding-left: 20px;")
244
+ # Save to cache
245
+ if self.current_database and self.current_collection:
246
+ print(f"[InfoPanel] Saving collection info to cache: db='{self.current_database}', coll='{self.current_collection}'")
247
+ self.cache_manager.update(
248
+ self.current_database,
249
+ self.current_collection,
250
+ user_inputs={'collection_info': collection_info}
251
+ )
252
+ print(f"[InfoPanel] ✓ Saved collection info to cache.")
292
253
 
293
254
  except Exception as e:
294
255
  self._update_label(self.collection_name_label, self.current_collection)
@@ -299,9 +260,89 @@ class InfoPanel(QWidget):
299
260
  self.schema_label.setStyleSheet("color: red; padding-left: 20px;")
300
261
  self.provider_details_label.setText("N/A")
301
262
 
302
- def set_collection(self, collection_name: str):
263
+ def _display_collection_info(self, collection_info: Dict[str, Any]):
264
+ """Display collection information (from cache or fresh query)."""
265
+ # Update basic info
266
+ self._update_label(self.collection_name_label, self.current_collection)
267
+
268
+ # Vector dimension
269
+ vector_dim = collection_info.get("vector_dimension", "Unknown")
270
+ self._update_label(self.vector_dim_label, str(vector_dim))
271
+
272
+ # Enable configure button if we have a valid dimension
273
+ self.configure_embedding_btn.setEnabled(
274
+ vector_dim != "Unknown" and isinstance(vector_dim, int)
275
+ )
276
+
277
+ # Update embedding model display
278
+ self._update_embedding_model_display(collection_info)
279
+
280
+ # Distance metric
281
+ distance = collection_info.get("distance_metric", "Unknown")
282
+ self._update_label(self.distance_metric_label, distance)
283
+
284
+ # Total points
285
+ count = collection_info.get("count", 0)
286
+ self._update_label(self.total_points_label, f"{count:,}")
287
+
288
+ # Metadata schema
289
+ metadata_fields = collection_info.get("metadata_fields", [])
290
+ if metadata_fields:
291
+ schema_text = "\n".join([f"• {field}" for field in sorted(metadata_fields)])
292
+ self.schema_label.setText(schema_text)
293
+ self.schema_label.setStyleSheet("color: white; padding-left: 20px; font-family: monospace;")
294
+ else:
295
+ self.schema_label.setText("No metadata fields found")
296
+ self.schema_label.setStyleSheet("color: gray; padding-left: 20px;")
297
+
298
+ # Provider-specific details
299
+ details_list = []
300
+
301
+ if isinstance(self.connection, ChromaDBConnection):
302
+ details_list.append("• Provider: ChromaDB")
303
+ details_list.append("• Supports: Documents, Metadata, Embeddings")
304
+ details_list.append("• Default embedding: all-MiniLM-L6-v2")
305
+
306
+ elif isinstance(self.connection, QdrantConnection):
307
+ details_list.append("• Provider: Qdrant")
308
+ details_list.append("• Supports: Points, Payload, Vectors")
309
+ # Get additional Qdrant-specific info if available
310
+ if "config" in collection_info:
311
+ config = collection_info["config"]
312
+ if "hnsw_config" in config:
313
+ hnsw = config["hnsw_config"]
314
+ details_list.append(f"• HNSW M: {hnsw.get('m', 'N/A')}")
315
+ details_list.append(f"• HNSW ef_construct: {hnsw.get('ef_construct', 'N/A')}")
316
+ if "optimizer_config" in config:
317
+ opt = config["optimizer_config"]
318
+ details_list.append(f"• Indexing threshold: {opt.get('indexing_threshold', 'N/A')}")
319
+
320
+ if details_list:
321
+ self.provider_details_label.setText("\n".join(details_list))
322
+ self.provider_details_label.setStyleSheet("color: white; padding-left: 20px; font-family: monospace;")
323
+ else:
324
+ self.provider_details_label.setText("No additional details available")
325
+ self.provider_details_label.setStyleSheet("color: gray; padding-left: 20px;")
326
+
327
+ def set_collection(self, collection_name: str, database_name: str = ""):
303
328
  """Set the current collection and refresh its information."""
304
329
  self.current_collection = collection_name
330
+ # Always update database_name if provided
331
+ if database_name:
332
+ self.current_database = database_name
333
+ self.connection_id = database_name # database_name is the connection ID
334
+
335
+ print(f"[InfoPanel] Setting collection: db='{self.current_database}', coll='{collection_name}'")
336
+
337
+ # Check cache first for collection info
338
+ cached = self.cache_manager.get(self.current_database, self.current_collection)
339
+ if cached and hasattr(cached, 'user_inputs') and cached.user_inputs.get('collection_info'):
340
+ print(f"[InfoPanel] ✓ Cache HIT! Loading collection info from cache.")
341
+ collection_info = cached.user_inputs['collection_info']
342
+ self._display_collection_info(collection_info)
343
+ return
344
+
345
+ print(f"[InfoPanel] ✗ Cache MISS. Loading collection info from database...")
305
346
  self.refresh_collection_info()
306
347
 
307
348
  def _update_label(self, row_widget: QWidget, value: str):
@@ -309,3 +350,108 @@ class InfoPanel(QWidget):
309
350
  value_label = row_widget.property("value_label")
310
351
  if value_label and isinstance(value_label, QLabel):
311
352
  value_label.setText(value)
353
+
354
+ def _update_embedding_model_display(self, collection_info: Dict[str, Any]):
355
+ """Update the embedding model label based on current configuration."""
356
+ from ...services.settings_service import SettingsService
357
+
358
+ # Check if stored in collection metadata
359
+ if 'embedding_model' in collection_info:
360
+ model_name = collection_info['embedding_model']
361
+ model_type = collection_info.get('embedding_model_type', 'unknown')
362
+ self.embedding_model_label.setText(f"{model_name} ({model_type})")
363
+ self.embedding_model_label.setStyleSheet("color: lightgreen;")
364
+ return
365
+
366
+ # Check user settings
367
+ settings = SettingsService()
368
+ collection_models = settings.get('collection_embedding_models', {})
369
+ collection_key = f"{self.connection_id}:{self.current_collection}"
370
+
371
+ if collection_key in collection_models:
372
+ model_info = collection_models[collection_key]
373
+ model_name = model_info['model']
374
+ model_type = model_info.get('type', 'unknown')
375
+ self.embedding_model_label.setText(f"{model_name} ({model_type})")
376
+ self.embedding_model_label.setStyleSheet("color: lightblue;")
377
+ return
378
+
379
+ # No configuration - using auto-detect
380
+ self.embedding_model_label.setText("Auto-detect (dimension-based)")
381
+ self.embedding_model_label.setStyleSheet("color: orange;")
382
+
383
+ def _configure_embedding_model(self):
384
+ """Open dialog to configure embedding model for current collection."""
385
+ if not self.current_collection:
386
+ return
387
+
388
+ from ..dialogs.embedding_config_dialog import EmbeddingConfigDialog
389
+ from ...services.settings_service import SettingsService
390
+
391
+ # Get current collection info
392
+ collection_info = self.connection.get_collection_info(self.current_collection)
393
+ if not collection_info:
394
+ return
395
+
396
+ vector_dim = collection_info.get("vector_dimension")
397
+ if not vector_dim or vector_dim == "Unknown":
398
+ return
399
+
400
+ # Get current configuration if any
401
+ settings = SettingsService()
402
+ collection_models = settings.get('collection_embedding_models', {})
403
+ collection_key = f"{self.connection_id}:{self.current_collection}"
404
+
405
+ current_model = None
406
+ current_type = None
407
+
408
+ # Check metadata first
409
+ if 'embedding_model' in collection_info:
410
+ current_model = collection_info['embedding_model']
411
+ current_type = collection_info.get('embedding_model_type')
412
+ # Then check settings
413
+ elif collection_key in collection_models:
414
+ model_info = collection_models[collection_key]
415
+ current_model = model_info.get('model')
416
+ current_type = model_info.get('type')
417
+
418
+ # Open dialog
419
+ dialog = EmbeddingConfigDialog(
420
+ self.current_collection,
421
+ vector_dim,
422
+ current_model,
423
+ current_type,
424
+ self
425
+ )
426
+
427
+ result = dialog.exec()
428
+
429
+ if result == QDialog.DialogCode.Accepted:
430
+ # Save the configuration
431
+ selection = dialog.get_selection()
432
+ if selection:
433
+ model_name, model_type = selection
434
+
435
+ if collection_key not in collection_models:
436
+ collection_models[collection_key] = {}
437
+
438
+ collection_models[collection_key]['model'] = model_name
439
+ collection_models[collection_key]['type'] = model_type
440
+
441
+ settings.set('collection_embedding_models', collection_models)
442
+
443
+ # Refresh display
444
+ self._update_embedding_model_display(collection_info)
445
+
446
+ print(f"✓ Configured embedding model for '{self.current_collection}': {model_name} ({model_type})")
447
+
448
+ elif result == 2: # Clear configuration
449
+ # Remove from settings
450
+ if collection_key in collection_models:
451
+ del collection_models[collection_key]
452
+ settings.set('collection_embedding_models', collection_models)
453
+
454
+ # Refresh display
455
+ self._update_embedding_model_display(collection_info)
456
+
457
+ print(f"✓ Cleared embedding model configuration for '{self.current_collection}'")